diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9300cc846..6de956328 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -85,6 +85,12 @@ All pull requests must comply with the above requirements and with the [stylegui Translations are managed on [Transifex](https://www.transifex.com/keepassxc/keepassxc/) which offers a web interface. Please join an existing language team or request a new one if there is none. +If you open a Pull Request with new strings that require translations, you will need to run the following: +``` +./release-tool i18n lupdate +``` +This will make the new strings available for translation in Transifex. + ## Styleguides ### Git branch strategy diff --git a/docs/man/keepassxc-cli.1.adoc b/docs/man/keepassxc-cli.1.adoc index 8ae0f4693..f5a5ac55a 100644 --- a/docs/man/keepassxc-cli.1.adoc +++ b/docs/man/keepassxc-cli.1.adoc @@ -66,6 +66,11 @@ It provides the ability to query and modify the entries of a KeePass database, d 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. +*db-edit* [_options_] <__database__>:: + Edits a database. + When setting a key file, the key file will be created if the file that is referred to + does not exist. + *db-info* [_options_] <__database__>:: Show a database's information. @@ -154,7 +159,7 @@ It provides the ability to query and modify the entries of a KeePass database, d *--no-password*:: Deactivates the password key for the database. -*-y*, *--yubikey* <__slot__>:: +*-y*, *--yubikey* <__slot[:serial]__>:: Specifies a yubikey slot for unlocking the database. In a merge operation this option is used to specify the YubiKey slot for the first database. @@ -177,7 +182,7 @@ It provides the ability to query and modify the entries of a KeePass database, d *--no-password-from*:: Deactivates password key for the database to merge from. -*--yubikey-from* <__slot__>:: +*--yubikey-from* <__slot[:serial]__>:: YubiKey slot for the second database. *-s*, *--same-credentials*:: @@ -235,16 +240,24 @@ The same password generation options as documented for the generate command can If a unique matching entry is found it will be copied to the clipboard. If multiple entries are found they will be listed to refine the search. (no clip performed) -=== Create and Import options -*-k*, *--set-key-file* <__path__>:: +=== Db-create, Db-edit and Import options +*--set-key-file* <__path__>:: Set the key file for the database. *-p*, *--set-password*:: Set a password for the database. +=== Db-create, Import options *-t*, *--decryption-time* <__time__>:: Target decryption time in MS for the database. +=== Db-edit options +*--unset-password* <__path__>:: + Removes the password for the database. + +*--unset-key-file* <__path__>:: + Removes the key file for the database. + === Show options *-a*, *--attributes* <__attribute__>...:: Shows the named attributes. diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 7c4521579..2701f7b05 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -7871,6 +7871,59 @@ Kernel: %3 %4 Show all the attributes of the entry. + + Edit a database. + + + + Could not change the database key. + + + + Database was not modified. + + + + Successfully edited the database. + + + + Loading the new key file failed: %1 + + + + Unset the password for the database. + + + + Unset the key file for the database. + + + + Cannot use %1 and %2 at the same time. + + + + Cannot remove all the keys from a database. + + + + Cannot remove password: The database does not have a password. + + + + Cannot remove file key: The database does not have a file key. + + + + Found unexpected Key type %1 + + + + Set the key file for the database. +This options is deprecated, use --set-key-file instead. + + QtIOCompressor diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 984427084..8f0b2854a 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -22,9 +22,11 @@ set(cli_SOURCES AttachmentRemove.cpp Clip.cpp Close.cpp - Create.cpp Command.cpp DatabaseCommand.cpp + DatabaseCreate.cpp + DatabaseEdit.cpp + DatabaseInfo.cpp Diceware.cpp Edit.cpp Estimate.cpp @@ -33,7 +35,6 @@ set(cli_SOURCES Generate.cpp Help.cpp Import.cpp - Info.cpp List.cpp Merge.cpp Move.cpp diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index 2eab320a3..4bba8fff9 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -23,7 +23,9 @@ #include "AttachmentRemove.h" #include "Clip.h" #include "Close.h" -#include "Create.h" +#include "DatabaseCreate.h" +#include "DatabaseEdit.h" +#include "DatabaseInfo.h" #include "Diceware.h" #include "Edit.h" #include "Estimate.h" @@ -32,7 +34,6 @@ #include "Generate.h" #include "Help.h" #include "Import.h" -#include "Info.h" #include "List.h" #include "Merge.h" #include "Move.h" @@ -172,8 +173,9 @@ namespace Commands s_commands.insert(QStringLiteral("attachment-rm"), QSharedPointer(new AttachmentRemove())); s_commands.insert(QStringLiteral("clip"), QSharedPointer(new Clip())); s_commands.insert(QStringLiteral("close"), QSharedPointer(new Close())); - s_commands.insert(QStringLiteral("db-create"), QSharedPointer(new Create())); - s_commands.insert(QStringLiteral("db-info"), QSharedPointer(new Info())); + s_commands.insert(QStringLiteral("db-create"), QSharedPointer(new DatabaseCreate())); + s_commands.insert(QStringLiteral("db-edit"), QSharedPointer(new DatabaseEdit())); + s_commands.insert(QStringLiteral("db-info"), QSharedPointer(new DatabaseInfo())); 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/Create.cpp b/src/cli/DatabaseCreate.cpp similarity index 74% rename from src/cli/Create.cpp rename to src/cli/DatabaseCreate.cpp index 760bf5368..10b424825 100644 --- a/src/cli/Create.cpp +++ b/src/cli/DatabaseCreate.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "Create.h" +#include "DatabaseCreate.h" #include "Utils.h" #include "keys/FileKey.h" @@ -23,34 +23,39 @@ #include #include -const QCommandLineOption Create::DecryptionTimeOption = +const QCommandLineOption DatabaseCreate::DecryptionTimeOption = QCommandLineOption(QStringList() << "t" << "decryption-time", QObject::tr("Target decryption time in MS for the database."), QObject::tr("time")); -const QCommandLineOption Create::SetKeyFileOption = - QCommandLineOption(QStringList() << "k" - << "set-key-file", +const QCommandLineOption DatabaseCreate::SetKeyFileShortOption = QCommandLineOption( + QStringList() << "k", + QObject::tr("Set the key file for the database.\nThis options is deprecated, use --set-key-file instead."), + QObject::tr("path")); + +const QCommandLineOption DatabaseCreate::SetKeyFileOption = + QCommandLineOption(QStringList() << "set-key-file", QObject::tr("Set the key file for the database."), QObject::tr("path")); -const QCommandLineOption Create::SetPasswordOption = +const QCommandLineOption DatabaseCreate::SetPasswordOption = QCommandLineOption(QStringList() << "p" << "set-password", QObject::tr("Set a password for the database.")); -Create::Create() +DatabaseCreate::DatabaseCreate() { name = QString("db-create"); description = QObject::tr("Create a new database."); positionalArguments.append({QString("database"), QObject::tr("Path of the database."), QString("")}); - options.append(Create::SetKeyFileOption); - options.append(Create::SetPasswordOption); - options.append(Create::DecryptionTimeOption); + options.append(DatabaseCreate::SetKeyFileOption); + options.append(DatabaseCreate::SetKeyFileShortOption); + options.append(DatabaseCreate::SetPasswordOption); + options.append(DatabaseCreate::DecryptionTimeOption); } -QSharedPointer Create::initializeDatabaseFromOptions(const QSharedPointer& parser) +QSharedPointer DatabaseCreate::initializeDatabaseFromOptions(const QSharedPointer& parser) { if (parser.isNull()) { return {}; @@ -60,7 +65,7 @@ QSharedPointer Create::initializeDatabaseFromOptions(const QSharedPoin auto& err = Utils::STDERR; // Validate the decryption time before asking for a password. - QString decryptionTimeValue = parser->value(Create::DecryptionTimeOption); + QString decryptionTimeValue = parser->value(DatabaseCreate::DecryptionTimeOption); int decryptionTime = 0; if (decryptionTimeValue.length() != 0) { decryptionTime = decryptionTimeValue.toInt(); @@ -78,7 +83,7 @@ QSharedPointer Create::initializeDatabaseFromOptions(const QSharedPoin auto key = QSharedPointer::create(); - if (parser->isSet(Create::SetPasswordOption)) { + if (parser->isSet(DatabaseCreate::SetPasswordOption)) { auto passwordKey = Utils::getConfirmedPassword(); if (passwordKey.isNull()) { err << QObject::tr("Failed to set database password.") << endl; @@ -87,10 +92,18 @@ QSharedPointer Create::initializeDatabaseFromOptions(const QSharedPoin key->addKey(passwordKey); } - if (parser->isSet(Create::SetKeyFileOption)) { + if (parser->isSet(DatabaseCreate::SetKeyFileOption) || parser->isSet(DatabaseCreate::SetKeyFileShortOption)) { QSharedPointer fileKey; - if (!Utils::loadFileKey(parser->value(Create::SetKeyFileOption), fileKey)) { + QString keyFilePath; + if (parser->isSet(DatabaseCreate::SetKeyFileShortOption)) { + qWarning("The -k option will be deprecated. Please use the --set-key-file option instead."); + keyFilePath = parser->value(DatabaseCreate::SetKeyFileShortOption); + } else { + keyFilePath = parser->value(DatabaseCreate::SetKeyFileOption); + } + + if (!Utils::loadFileKey(keyFilePath, fileKey)) { err << QObject::tr("Loading the key file failed") << endl; return {}; } @@ -141,7 +154,7 @@ QSharedPointer Create::initializeDatabaseFromOptions(const QSharedPoin * * @return EXIT_SUCCESS on success, or EXIT_FAILURE on failure */ -int Create::execute(const QStringList& arguments) +int DatabaseCreate::execute(const QStringList& arguments) { QSharedPointer parser = getCommandLineParser(arguments); if (parser.isNull()) { @@ -159,7 +172,7 @@ int Create::execute(const QStringList& arguments) return EXIT_FAILURE; } - QSharedPointer db = Create::initializeDatabaseFromOptions(parser); + QSharedPointer db = DatabaseCreate::initializeDatabaseFromOptions(parser); if (!db) { return EXIT_FAILURE; } diff --git a/src/cli/Create.h b/src/cli/DatabaseCreate.h similarity index 82% rename from src/cli/Create.h rename to src/cli/DatabaseCreate.h index 3b91dca10..30db16ed9 100644 --- a/src/cli/Create.h +++ b/src/cli/DatabaseCreate.h @@ -15,22 +15,23 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_CREATE_H -#define KEEPASSXC_CREATE_H +#ifndef KEEPASSXC_DATABASECREATE_H +#define KEEPASSXC_DATABASECREATE_H #include "Command.h" -class Create : public Command +class DatabaseCreate : public Command { public: - Create(); + DatabaseCreate(); int execute(const QStringList& arguments) override; static QSharedPointer initializeDatabaseFromOptions(const QSharedPointer& parser); static const QCommandLineOption SetKeyFileOption; + static const QCommandLineOption SetKeyFileShortOption; static const QCommandLineOption SetPasswordOption; static const QCommandLineOption DecryptionTimeOption; }; -#endif // KEEPASSXC_CREATE_H +#endif // KEEPASSXC_DATABASECREATE_H diff --git a/src/cli/DatabaseEdit.cpp b/src/cli/DatabaseEdit.cpp new file mode 100644 index 000000000..f5ca4ef2e --- /dev/null +++ b/src/cli/DatabaseEdit.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 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 "DatabaseEdit.h" + +#include "Utils.h" +#include "cli/DatabaseCreate.h" +#include "keys/ChallengeResponseKey.h" +#include "keys/FileKey.h" +#include "keys/PasswordKey.h" + +#include +#include + +const QCommandLineOption DatabaseEdit::UnsetPasswordOption = + QCommandLineOption(QStringList() << "unset-password", QObject::tr("Unset the password for the database.")); +const QCommandLineOption DatabaseEdit::UnsetKeyFileOption = + QCommandLineOption(QStringList() << "unset-key-file", QObject::tr("Unset the key file for the database.")); + +DatabaseEdit::DatabaseEdit() +{ + name = QString("db-edit"); + description = QObject::tr("Edit a database."); + options.append(DatabaseCreate::SetKeyFileOption); + options.append(DatabaseCreate::SetPasswordOption); + options.append(DatabaseEdit::UnsetKeyFileOption); + options.append(DatabaseEdit::UnsetPasswordOption); +} + +int DatabaseEdit::executeWithDatabase(QSharedPointer database, QSharedPointer parser) +{ + auto& out = Utils::STDOUT; + auto& err = Utils::STDERR; + + const QStringList args = parser->positionalArguments(); + bool databaseWasChanged = false; + + if (parser->isSet(DatabaseCreate::SetPasswordOption) && parser->isSet(DatabaseEdit::UnsetPasswordOption)) { + err << QObject::tr("Cannot use %1 and %2 at the same time.") + .arg(DatabaseCreate::SetPasswordOption.names().at(0)) + .arg(DatabaseEdit::UnsetPasswordOption.names().at(0)) + << endl; + return EXIT_FAILURE; + } + + if (parser->isSet(DatabaseCreate::SetKeyFileOption) && parser->isSet(DatabaseEdit::UnsetKeyFileOption)) { + err << QObject::tr("Cannot use %1 and %2 at the same time.") + .arg(DatabaseCreate::SetKeyFileOption.names().at(0)) + .arg(DatabaseEdit::UnsetKeyFileOption.names().at(0)) + << endl; + return EXIT_FAILURE; + } + + bool hasKeyChange = + (parser->isSet(DatabaseCreate::SetPasswordOption) || parser->isSet(DatabaseCreate::SetKeyFileOption) + || parser->isSet(DatabaseEdit::UnsetPasswordOption) || parser->isSet(DatabaseEdit::UnsetKeyFileOption)); + + if (hasKeyChange) { + auto newDatabaseKey = getNewDatabaseKey(database, + parser->isSet(DatabaseCreate::SetPasswordOption), + parser->isSet(DatabaseEdit::UnsetPasswordOption), + parser->value(DatabaseCreate::SetKeyFileOption), + parser->isSet(DatabaseEdit::UnsetKeyFileOption)); + if (newDatabaseKey.isNull()) { + err << QObject::tr("Could not change the database key.") << endl; + return EXIT_FAILURE; + } + database->setKey(newDatabaseKey); + databaseWasChanged = true; + } + + if (!databaseWasChanged) { + out << QObject::tr("Database was not modified.") << endl; + return EXIT_SUCCESS; + } + + QString errorMessage; + if (!database->save(Database::Atomic, {}, &errorMessage)) { + err << QObject::tr("Writing the database failed: %1").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + + out << QObject::tr("Successfully edited the database.") << endl; + return EXIT_SUCCESS; +} + +QSharedPointer DatabaseEdit::getNewDatabaseKey(QSharedPointer database, + bool updatePassword, + bool removePassword, + QString newFileKeyPath, + bool removeKeyFile) +{ + auto& err = Utils::STDERR; + auto newDatabaseKey = QSharedPointer::create(); + bool updateKeyFile = !newFileKeyPath.isEmpty(); + + auto currentPasswordKey = database->key()->getKey(PasswordKey::UUID); + auto currentFileKey = database->key()->getKey(FileKey::UUID); + auto currentChallengeResponseKey = database->key()->getChallengeResponseKey(ChallengeResponseKey::UUID); + + if (removePassword && currentPasswordKey.isNull()) { + err << QObject::tr("Cannot remove password: The database does not have a password.") << endl; + return {}; + } + + if (removeKeyFile && currentFileKey.isNull()) { + err << QObject::tr("Cannot remove file key: The database does not have a file key.") << endl; + return {}; + } + + if (updatePassword) { + QSharedPointer newPasswordKey = Utils::getConfirmedPassword(); + if (newPasswordKey.isNull()) { + err << QObject::tr("Failed to set database password.") << endl; + return {}; + } + newDatabaseKey->addKey(newPasswordKey); + } else if (!removePassword && !currentPasswordKey.isNull()) { + newDatabaseKey->addKey(currentPasswordKey); + } + + if (updateKeyFile) { + QSharedPointer newFileKey = QSharedPointer::create(); + QString errorMessage; + if (!Utils::loadFileKey(newFileKeyPath, newFileKey)) { + err << QObject::tr("Loading the new key file failed: %1").arg(errorMessage) << endl; + return {}; + } + newDatabaseKey->addKey(newFileKey); + } else if (!removeKeyFile && !currentFileKey.isNull()) { + newDatabaseKey->addKey(currentFileKey); + } + + // This is a sanity check to make sure that this function is not used if + // new key types are introduced. Otherwise, those key types would be + // silently removed from the database. + for (const QSharedPointer& key : database->key()->keys()) { + if (key->uuid() != PasswordKey::UUID && key->uuid() != FileKey::UUID) { + err << QObject::tr("Found unexpected Key type %1").arg(key->uuid().toString()) << endl; + return {}; + } + } + for (const QSharedPointer& key : database->key()->challengeResponseKeys()) { + if (key->uuid() != ChallengeResponseKey::UUID) { + err << QObject::tr("Found unexpected Key type %1").arg(key->uuid().toString()) << endl; + return {}; + } + } + + if (!currentChallengeResponseKey.isNull()) { + newDatabaseKey->addChallengeResponseKey(currentChallengeResponseKey); + } + + if (newDatabaseKey->keys().isEmpty() && newDatabaseKey->challengeResponseKeys().isEmpty()) { + err << QObject::tr("Cannot remove all the keys from a database.") << endl; + return {}; + } + + return newDatabaseKey; +} diff --git a/src/cli/DatabaseEdit.h b/src/cli/DatabaseEdit.h new file mode 100644 index 000000000..2d77b206f --- /dev/null +++ b/src/cli/DatabaseEdit.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 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_DATABASEEDIT_H +#define KEEPASSXC_DATABASEEDIT_H + +#include "DatabaseCommand.h" + +class DatabaseEdit : public DatabaseCommand +{ +public: + DatabaseEdit(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override; + + static const QCommandLineOption UnsetKeyFileOption; + static const QCommandLineOption UnsetPasswordOption; + +private: + QSharedPointer getNewDatabaseKey(QSharedPointer database, + bool updatePassword, + bool removePassword, + QString newFileKeyPath, + bool removeKeyFile); +}; + +#endif // KEEPASSXC_DATABASEEDIT_H diff --git a/src/cli/Info.cpp b/src/cli/DatabaseInfo.cpp similarity index 95% rename from src/cli/Info.cpp rename to src/cli/DatabaseInfo.cpp index bf093664a..b5569ae84 100644 --- a/src/cli/Info.cpp +++ b/src/cli/DatabaseInfo.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "Info.h" +#include "DatabaseInfo.h" #include "Utils.h" #include "core/DatabaseStats.h" @@ -25,13 +25,13 @@ #include -Info::Info() +DatabaseInfo::DatabaseInfo() { name = QString("db-info"); description = QObject::tr("Show a database's information."); } -int Info::executeWithDatabase(QSharedPointer database, QSharedPointer) +int DatabaseInfo::executeWithDatabase(QSharedPointer database, QSharedPointer) { auto& out = Utils::STDOUT; diff --git a/src/cli/Info.h b/src/cli/DatabaseInfo.h similarity index 84% rename from src/cli/Info.h rename to src/cli/DatabaseInfo.h index 386dc62eb..42a48c86f 100644 --- a/src/cli/Info.h +++ b/src/cli/DatabaseInfo.h @@ -15,17 +15,17 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_INFO_H -#define KEEPASSXC_INFO_H +#ifndef KEEPASSXC_DATABASEINFO_H +#define KEEPASSXC_DATABASEINFO_H #include "DatabaseCommand.h" -class Info : public DatabaseCommand +class DatabaseInfo : public DatabaseCommand { public: - Info(); + DatabaseInfo(); int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override; }; -#endif // KEEPASSXC_INFO_H +#endif // KEEPASSXC_DATABASEINFO_H diff --git a/src/cli/Import.cpp b/src/cli/Import.cpp index 3732f0cf6..cc74df767 100644 --- a/src/cli/Import.cpp +++ b/src/cli/Import.cpp @@ -17,7 +17,7 @@ #include "Import.h" -#include "Create.h" +#include "DatabaseCreate.h" #include "Utils.h" #include @@ -40,9 +40,10 @@ Import::Import() description = QObject::tr("Import the contents of an XML database."); positionalArguments.append({QString("xml"), QObject::tr("Path of the XML database export."), QString("")}); positionalArguments.append({QString("database"), QObject::tr("Path of the new database."), QString("")}); - options.append(Create::SetKeyFileOption); - options.append(Create::SetPasswordOption); - options.append(Create::DecryptionTimeOption); + options.append(DatabaseCreate::SetKeyFileOption); + options.append(DatabaseCreate::SetKeyFileShortOption); + options.append(DatabaseCreate::SetPasswordOption); + options.append(DatabaseCreate::DecryptionTimeOption); } int Import::execute(const QStringList& arguments) @@ -64,7 +65,7 @@ int Import::execute(const QStringList& arguments) return EXIT_FAILURE; } - QSharedPointer db = Create::initializeDatabaseFromOptions(parser); + QSharedPointer db = DatabaseCreate::initializeDatabaseFromOptions(parser); if (!db) { return EXIT_FAILURE; } diff --git a/src/keys/CompositeKey.cpp b/src/keys/CompositeKey.cpp index 0fdb2b32f..705491667 100644 --- a/src/keys/CompositeKey.cpp +++ b/src/keys/CompositeKey.cpp @@ -169,6 +169,36 @@ void CompositeKey::addKey(const QSharedPointer& key) m_keys.append(key); } +/** + * Get the \link Key with the specified ID. + * + * @param keyId the ID of the key to get. + */ +QSharedPointer CompositeKey::getKey(const QUuid keyId) const +{ + for (const QSharedPointer& key : m_keys) { + if (key->uuid() == keyId) { + return key; + } + } + return {}; +} + +/** + * Get the \link ChallengeResponseKey with the specified ID. + * + * @param keyId the ID of the key to get. + */ +QSharedPointer CompositeKey::getChallengeResponseKey(const QUuid keyId) const +{ + for (const QSharedPointer& key : m_challengeResponseKeys) { + if (key->uuid() == keyId) { + return key; + } + } + return {}; +} + /** * @return list of Keys which are part of this CompositeKey */ diff --git a/src/keys/CompositeKey.h b/src/keys/CompositeKey.h index 8db385c2c..451f88e4f 100644 --- a/src/keys/CompositeKey.h +++ b/src/keys/CompositeKey.h @@ -43,6 +43,8 @@ public: bool challenge(const QByteArray& seed, QByteArray& result, QString* error = nullptr) const; void addKey(const QSharedPointer& key); + QSharedPointer getKey(const QUuid keyType) const; + QSharedPointer getChallengeResponseKey(const QUuid keyType) const; const QList>& keys() const; void addChallengeResponseKey(const QSharedPointer& key); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 785121032..2ccda2e24 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -34,7 +34,9 @@ #include "cli/AttachmentImport.h" #include "cli/AttachmentRemove.h" #include "cli/Clip.h" -#include "cli/Create.h" +#include "cli/DatabaseCreate.h" +#include "cli/DatabaseEdit.h" +#include "cli/DatabaseInfo.h" #include "cli/Diceware.h" #include "cli/Edit.h" #include "cli/Estimate.h" @@ -42,7 +44,6 @@ #include "cli/Generate.h" #include "cli/Help.h" #include "cli/Import.h" -#include "cli/Info.h" #include "cli/List.h" #include "cli/Merge.h" #include "cli/Move.h" @@ -242,7 +243,7 @@ void TestCli::testBatchCommands() QVERIFY(Commands::getCommand("show")); QVERIFY(Commands::getCommand("search")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 25); + QCOMPARE(Commands::getCommands().size(), 26); } void TestCli::testInteractiveCommands() @@ -274,7 +275,7 @@ void TestCli::testInteractiveCommands() QVERIFY(Commands::getCommand("show")); QVERIFY(Commands::getCommand("search")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 25); + QCOMPARE(Commands::getCommands().size(), 26); } void TestCli::testAdd() @@ -732,7 +733,7 @@ void TestCli::testClip() void TestCli::testCreate() { - Create createCmd; + DatabaseCreate createCmd; QVERIFY(!createCmd.name.isEmpty()); QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name)); @@ -848,9 +849,147 @@ void TestCli::testCreate() QVERIFY(db); } +void TestCli::testDatabaseEdit() +{ + TemporaryFile firstKeyFile; + firstKeyFile.open(); + firstKeyFile.write(QString("keyFilePassword").toLatin1()); + firstKeyFile.close(); + + TemporaryFile secondKeyFile; + secondKeyFile.open(); + secondKeyFile.write(QString("newKeyFilePassword").toLatin1()); + secondKeyFile.close(); + + QScopedPointer testDir(new QTemporaryDir()); + + DatabaseCreate createCmd; + DatabaseEdit editCmd; + QVERIFY(!editCmd.name.isEmpty()); + QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name)); + + QString dbFilename; + dbFilename = testDir->path() + "/testDatabaseEdit.kdbx"; + + // Creating a database for testing + setInput({"a", "a"}); + execCmd(createCmd, {"db-create", dbFilename, "-p"}); + QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n")); + + // Sanity check. + auto db = readDatabase(dbFilename, "a"); + QVERIFY(!db.isNull()); + + setInput("a"); + execCmd(editCmd, {"db-edit", dbFilename, "-p", "--unset-password"}); + QCOMPARE(m_stdout->readAll(), QByteArray("")); + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("Cannot use p and unset-password at the same time.\n")); + + setInput("a"); + execCmd(editCmd, {"db-edit", dbFilename, "--set-key-file", "/key/file/path", "--unset-key-file"}); + QCOMPARE(m_stdout->readAll(), QByteArray("")); + // Skipping the password prompt. + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("Cannot use set-key-file and unset-key-file at the same time.\n")); + + // Sanity check. + db = readDatabase(dbFilename, "a"); + QVERIFY(!db.isNull()); + + setInput({"a", "b", "b"}); + execCmd(editCmd, {"db-edit", dbFilename, "-p"}); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + // Sanity check + db = readDatabase(dbFilename, "b"); + QVERIFY(!db.isNull()); + + setInput("b"); + execCmd(editCmd, {"db-edit", dbFilename, "--set-key-file", firstKeyFile.fileName()}); + // Skipping the password prompt. + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("")); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + // Sanity check + db = readDatabase(dbFilename, "b"); + QVERIFY(db.isNull()); + db = readDatabase(dbFilename, "b", firstKeyFile.fileName()); + QVERIFY(!db.isNull()); + + setInput("b"); + execCmd(editCmd, + {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--set-key-file", secondKeyFile.fileName()}); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + // Sanity check + db = readDatabase(dbFilename, "b", firstKeyFile.fileName()); + QVERIFY(db.isNull()); + db = readDatabase(dbFilename, "b", secondKeyFile.fileName()); + QVERIFY(!db.isNull()); + + setInput("b"); + execCmd(editCmd, {"db-edit", dbFilename, "-k", secondKeyFile.fileName(), "--unset-password"}); + // Skipping the password prompt. + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("")); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + execCmd(editCmd, + {"db-edit", + dbFilename, + "--no-password", + "-k", + secondKeyFile.fileName(), + "--set-key-file", + firstKeyFile.fileName()}); + // Skipping the password prompt. + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("")); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + setInput({"b", "b"}); + execCmd(editCmd, {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--no-password", "--set-password"}); + // Skipping over the password setting prompts. + m_stderr->readLine(); + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("")); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + setInput("b"); + execCmd(editCmd, {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--unset-key-file"}); + // Skipping the password prompt. + m_stderr->readLine(); + QCOMPARE(m_stderr->readAll(), QByteArray("")); + QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n")); + + // Sanity check + db = readDatabase(dbFilename, "b", firstKeyFile.fileName()); + QVERIFY(db.isNull()); + db = readDatabase(dbFilename, "b"); + QVERIFY(!db.isNull()); + + // Trying to remove the key file when there is none set should + // raise an error. + setInput("b"); + execCmd(editCmd, {"db-edit", dbFilename, "-p", "--unset-key-file"}); + QCOMPARE(m_stdout->readAll(), QByteArray("")); + m_stderr->readLine(); + QCOMPARE(m_stderr->readLine(), QByteArray("Cannot remove file key: The database does not have a file key.\n")); + QCOMPARE(m_stderr->readLine(), QByteArray("Could not change the database key.\n")); + + setInput("b"); + execCmd(editCmd, {"db-edit", dbFilename, "--unset-password"}); + QCOMPARE(m_stdout->readAll(), QByteArray("")); + // Skipping the password prompt. + m_stderr->readLine(); + QCOMPARE(m_stderr->readLine(), QByteArray("Cannot remove all the keys from a database.\n")); +} + void TestCli::testInfo() { - Info infoCmd; + DatabaseInfo infoCmd; QVERIFY(!infoCmd.name.isEmpty()); QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name)); @@ -1613,7 +1752,7 @@ void TestCli::testMerge() void TestCli::testMergeWithKeys() { - Create createCmd; + DatabaseCreate createCmd; QVERIFY(!createCmd.name.isEmpty()); QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name)); diff --git a/tests/TestCli.h b/tests/TestCli.h index bbe80a4b3..3beb80724 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -54,6 +54,7 @@ private slots: void testCommandParsing_data(); void testCommandParsing(); void testCreate(); + void testDatabaseEdit(); void testDiceware(); void testEdit(); void testEstimate_data();