Add keyfile option to keepassxc cli import cmd (#5402)

Fixes #5311

Added the keyFile logic from the create command to the import command and moved the loadFileKey() function
to the Utils class since it is now used in both create & import classes.
This commit is contained in:
Bernhard Berg 2020-10-10 02:31:29 +02:00 committed by GitHub
parent bf2cad28af
commit fd3cc7e8c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 124 deletions

View File

@ -83,7 +83,12 @@ It provides the ability to query and modify the entries of a KeePass database, d
Displays a list of available commands, or detailed information about the specified command.
*import* [_options_] <__xml__> <__database__>::
Imports the contents of an XML database to the target database.
Imports the contents of an XML exported database to a new created database
with a password and/or key file.
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.
The new database will be in kdbx 4 format.
*locate* [_options_] <__database__> <__term__>::
Locates all the entries that match a specific search term in a database.
@ -219,7 +224,7 @@ 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 options
=== Create and Import options
*-k*, *--set-key-file* <__path__>::
Set the key file for the database.

View File

@ -28,6 +28,7 @@
#include "core/Database.h"
#include "keys/CompositeKey.h"
#include "keys/FileKey.h"
#include "keys/Key.h"
const QCommandLineOption Create::DecryptionTimeOption =
@ -57,6 +58,84 @@ Create::Create()
options.append(Create::DecryptionTimeOption);
}
QSharedPointer<Database> Create::initializeDatabaseFromOptions(const QSharedPointer<QCommandLineParser>& parser)
{
if (parser.isNull()) {
return {};
}
auto& out = parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT;
auto& err = Utils::STDERR;
// Validate the decryption time before asking for a password.
QString decryptionTimeValue = parser->value(Create::DecryptionTimeOption);
int decryptionTime = 0;
if (decryptionTimeValue.length() != 0) {
decryptionTime = decryptionTimeValue.toInt();
if (decryptionTime <= 0) {
err << QObject::tr("Invalid decryption time %1.").arg(decryptionTimeValue) << endl;
return {};
}
if (decryptionTime < Kdf::MIN_ENCRYPTION_TIME || decryptionTime > Kdf::MAX_ENCRYPTION_TIME) {
err << QObject::tr("Target decryption time must be between %1 and %2.")
.arg(QString::number(Kdf::MIN_ENCRYPTION_TIME), QString::number(Kdf::MAX_ENCRYPTION_TIME))
<< endl;
return {};
}
}
auto key = QSharedPointer<CompositeKey>::create();
if (parser->isSet(Create::SetPasswordOption)) {
auto passwordKey = Utils::getConfirmedPassword();
if (passwordKey.isNull()) {
err << QObject::tr("Failed to set database password.") << endl;
return {};
}
key->addKey(passwordKey);
}
if (parser->isSet(Create::SetKeyFileOption)) {
QSharedPointer<FileKey> fileKey;
if (!Utils::loadFileKey(parser->value(Create::SetKeyFileOption), fileKey)) {
err << QObject::tr("Loading the key file failed") << endl;
return {};
}
if (!fileKey.isNull()) {
key->addKey(fileKey);
}
}
if (key->isEmpty()) {
err << QObject::tr("No key is set. Aborting database creation.") << endl;
return {};
}
auto db = QSharedPointer<Database>::create();
db->setKey(key);
if (decryptionTime != 0) {
auto kdf = db->kdf();
Q_ASSERT(kdf);
out << QObject::tr("Benchmarking key derivation function for %1ms delay.").arg(decryptionTimeValue) << endl;
int rounds = kdf->benchmark(decryptionTime);
out << QObject::tr("Setting %1 rounds for key derivation function.").arg(QString::number(rounds)) << endl;
kdf->setRounds(rounds);
bool ok = db->changeKdf(kdf);
if (!ok) {
err << QObject::tr("error while setting database key derivation settings.") << endl;
return {};
}
}
return db;
}
/**
* Create a database file using the command line. A key file and/or
* password can be specified to encrypt the password. If none is
@ -88,72 +167,11 @@ int Create::execute(const QStringList& arguments)
return EXIT_FAILURE;
}
// Validate the decryption time before asking for a password.
QString decryptionTimeValue = parser->value(Create::DecryptionTimeOption);
int decryptionTime = 0;
if (decryptionTimeValue.length() != 0) {
decryptionTime = decryptionTimeValue.toInt();
if (decryptionTime <= 0) {
err << QObject::tr("Invalid decryption time %1.").arg(decryptionTimeValue) << endl;
return EXIT_FAILURE;
}
if (decryptionTime < Kdf::MIN_ENCRYPTION_TIME || decryptionTime > Kdf::MAX_ENCRYPTION_TIME) {
err << QObject::tr("Target decryption time must be between %1 and %2.")
.arg(QString::number(Kdf::MIN_ENCRYPTION_TIME), QString::number(Kdf::MAX_ENCRYPTION_TIME))
<< endl;
return EXIT_FAILURE;
}
}
auto key = QSharedPointer<CompositeKey>::create();
if (parser->isSet(Create::SetPasswordOption)) {
auto passwordKey = Utils::getConfirmedPassword();
if (passwordKey.isNull()) {
err << QObject::tr("Failed to set database password.") << endl;
return EXIT_FAILURE;
}
key->addKey(passwordKey);
}
if (parser->isSet(Create::SetKeyFileOption)) {
QSharedPointer<FileKey> fileKey;
if (!loadFileKey(parser->value(Create::SetKeyFileOption), fileKey)) {
err << QObject::tr("Loading the key file failed") << endl;
return EXIT_FAILURE;
}
if (!fileKey.isNull()) {
key->addKey(fileKey);
}
}
if (key->isEmpty()) {
err << QObject::tr("No key is set. Aborting database creation.") << endl;
QSharedPointer<Database> db = Create::initializeDatabaseFromOptions(parser);
if (!db) {
return EXIT_FAILURE;
}
QSharedPointer<Database> db(new Database);
db->setKey(key);
if (decryptionTime != 0) {
auto kdf = db->kdf();
Q_ASSERT(kdf);
out << QObject::tr("Benchmarking key derivation function for %1ms delay.").arg(decryptionTimeValue) << endl;
int rounds = kdf->benchmark(decryptionTime);
out << QObject::tr("Setting %1 rounds for key derivation function.").arg(QString::number(rounds)) << endl;
kdf->setRounds(rounds);
bool ok = db->changeKdf(kdf);
if (!ok) {
err << QObject::tr("error while setting database key derivation settings.") << endl;
return EXIT_FAILURE;
}
}
QString errorMessage;
if (!db->saveAs(databaseFilename, &errorMessage, true, false)) {
err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl;
@ -161,40 +179,5 @@ int Create::execute(const QStringList& arguments)
}
out << QObject::tr("Successfully created new database.") << endl;
currentDatabase = db;
return EXIT_SUCCESS;
}
/**
* Load a key file from disk. When the path specified does not exist a
* new file will be generated. No folders will be generated so the parent
* folder of the specified file nees to exist
*
* If the key file cannot be loaded or created the function will fail.
*
* @param path Path to the key file to be loaded
* @param fileKey Resulting fileKey
* @return true if the key file was loaded succesfully
*/
bool Create::loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey)
{
auto& err = Utils::STDERR;
QString error;
fileKey = QSharedPointer<FileKey>(new FileKey());
if (!QFileInfo::exists(path)) {
fileKey->create(path, &error);
if (!error.isEmpty()) {
err << QObject::tr("Creating KeyFile %1 failed: %2").arg(path, error) << endl;
return false;
}
}
if (!fileKey->load(path, &error)) {
err << QObject::tr("Loading KeyFile %1 failed: %2").arg(path, error) << endl;
return false;
}
return true;
}

View File

@ -20,20 +20,17 @@
#include "Command.h"
#include "keys/FileKey.h"
class Create : public Command
{
public:
Create();
int execute(const QStringList& arguments) override;
static QSharedPointer<Database> initializeDatabaseFromOptions(const QSharedPointer<QCommandLineParser>& parser);
static const QCommandLineOption SetKeyFileOption;
static const QCommandLineOption SetPasswordOption;
static const QCommandLineOption DecryptionTimeOption;
private:
bool loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey);
};
#endif // KEEPASSXC_CREATE_H

View File

@ -22,12 +22,14 @@
#include <QString>
#include <QTextStream>
#include "Create.h"
#include "Import.h"
#include "cli/TextStream.h"
#include "cli/Utils.h"
#include "core/Database.h"
#include "keys/CompositeKey.h"
#include "keys/FileKey.h"
#include "keys/Key.h"
/**
@ -40,12 +42,16 @@
*
* @return EXIT_SUCCESS on success, or EXIT_FAILURE on failure
*/
Import::Import()
{
name = QString("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);
}
int Import::execute(const QStringList& arguments)
@ -67,31 +73,18 @@ int Import::execute(const QStringList& arguments)
return EXIT_FAILURE;
}
auto key = QSharedPointer<CompositeKey>::create();
auto passwordKey = Utils::getConfirmedPassword();
if (passwordKey.isNull()) {
err << QObject::tr("Failed to set database password.") << endl;
return EXIT_FAILURE;
}
key->addKey(passwordKey);
if (key->isEmpty()) {
err << QObject::tr("No key is set. Aborting database creation.") << endl;
QSharedPointer<Database> db = Create::initializeDatabaseFromOptions(parser);
if (!db) {
return EXIT_FAILURE;
}
QString errorMessage;
Database db;
db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2));
db.setKey(key);
if (!db.import(xmlExportPath, &errorMessage)) {
if (!db->import(xmlExportPath, &errorMessage)) {
err << QObject::tr("Unable to import XML database: %1").arg(errorMessage) << endl;
return EXIT_FAILURE;
}
if (!db.saveAs(dbPath, &errorMessage, true, false)) {
if (!db->saveAs(dbPath, &errorMessage, true, false)) {
err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl;
return EXIT_FAILURE;
}

View File

@ -371,4 +371,37 @@ namespace Utils
return result;
}
/**
* Load a key file from disk. When the path specified does not exist a
* new file will be generated. No folders will be generated so the parent
* folder of the specified file needs to exist
*
* If the key file cannot be loaded or created the function will fail.
*
* @param path Path to the key file to be loaded
* @param fileKey Resulting fileKey
* @return true if the key file was loaded succesfully
*/
bool loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey)
{
auto& err = Utils::STDERR;
QString error;
fileKey = QSharedPointer<FileKey>(new FileKey());
if (!QFileInfo::exists(path)) {
fileKey->create(path, &error);
if (!error.isEmpty()) {
err << QObject::tr("Creating KeyFile %1 failed: %2").arg(path, error) << endl;
return false;
}
}
if (!fileKey->load(path, &error)) {
err << QObject::tr("Loading KeyFile %1 failed: %2").arg(path, error) << endl;
return false;
}
return true;
}
} // namespace Utils

View File

@ -36,6 +36,7 @@ namespace Utils
void setDefaultTextStreams();
void setStdinEcho(bool enable);
bool loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey);
QString getPassword(bool quiet = false);
QSharedPointer<PasswordKey> getConfirmedPassword();
int clipText(const QString& text);

View File

@ -1057,7 +1057,7 @@ void TestCli::testImport()
QString databaseFilename = testDir->path() + "/testImport1.kdbx";
setInput({"a", "a"});
execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename});
execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename, "-p"});
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
@ -1076,12 +1076,67 @@ void TestCli::testImport()
QString errorMessage = QString("File " + databaseFilename + " already exists.\n");
QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
// Testing import with non-existing keyfile
databaseFilename = testDir->path() + "/testImport2.kdbx";
QString keyfilePath = testDir->path() + "/keyfile.txt";
setInput({"a", "a"});
execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
db = readDatabase(databaseFilename, "a", keyfilePath);
QVERIFY(db);
// Testing import with existing keyfile
databaseFilename = testDir->path() + "/testImport3.kdbx";
setInput({"a", "a"});
execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
db = readDatabase(databaseFilename, "a", keyfilePath);
QVERIFY(db);
// Invalid decryption time (format).
databaseFilename = testDir->path() + "/testCreate_time.kdbx";
execCmd(importCmd, {"import", "-p", "-t", "NAN", m_xmlFile->fileName(), databaseFilename});
QCOMPARE(m_stdout->readAll(), QByteArray());
QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
// Invalid decryption time (range).
execCmd(importCmd, {"import", "-p", "-t", "10", m_xmlFile->fileName(), databaseFilename});
QCOMPARE(m_stdout->readAll(), QByteArray());
QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
int encryptionTime = 500;
// Custom encryption time
setInput({"a", "a"});
int epochBefore = QDateTime::currentMSecsSinceEpoch();
execCmd(importCmd,
{"import", "-p", "-t", QString::number(encryptionTime), m_xmlFile->fileName(), databaseFilename});
// Removing 100ms to make sure we account for changes in computation time.
QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
db = readDatabase(databaseFilename, "a");
QVERIFY(db);
// Quiet option
QScopedPointer<QTemporaryDir> testDirQuiet(new QTemporaryDir());
QString databaseFilenameQuiet = testDirQuiet->path() + "/testImport2.kdbx";
setInput({"a", "a"});
execCmd(importCmd, {"import", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
execCmd(importCmd, {"import", "-p", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));