CLI: Add Import XML command (#3572)

The CLI now contains an "import" command that creates a new database from the specified XML export. The new database is in kdbx 4 format, and does not currently accept a keyfile in database creation.

This change is required to create new databases from XML backups.

Fixes #2458
This commit is contained in:
Jacob Sachs 2019-10-14 08:37:26 -04:00 committed by Jonathan White
parent 82cfedfa43
commit dbe15d32e5
14 changed files with 2127 additions and 1904 deletions

View File

@ -616,8 +616,7 @@ QList<Entry*> BrowserService::searchEntries(const QString& url, const StringPair
// Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
for (const StringPair& keyPair : keyList) {
QString key =
db->metadata()->customData()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + keyPair.first);
QString key = db->metadata()->customData()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + keyPair.first);
if (!key.isEmpty() && keyPair.second == key) {
return true;
}

View File

@ -29,6 +29,7 @@ set(cli_SOURCES
Export.cpp
Generate.cpp
Help.cpp
Import.cpp
List.cpp
Locate.cpp
Merge.cpp

View File

@ -36,6 +36,7 @@
#include "Export.h"
#include "Generate.h"
#include "Help.h"
#include "Import.h"
#include "List.h"
#include "Locate.h"
#include "Merge.h"
@ -180,6 +181,7 @@ namespace Commands
s_commands.insert(QStringLiteral("quit"), QSharedPointer<Command>(new Exit("quit")));
} else {
s_commands.insert(QStringLiteral("export"), QSharedPointer<Command>(new Export()));
s_commands.insert(QStringLiteral("import"), QSharedPointer<Command>(new Import()));
}
}

View File

@ -71,7 +71,7 @@ int Create::execute(const QStringList& arguments)
auto key = QSharedPointer<CompositeKey>::create();
auto password = getPasswordFromStdin();
auto password = Utils::getPasswordFromStdin();
if (!password.isNull()) {
key->addKey(password);
}
@ -107,28 +107,6 @@ int Create::execute(const QStringList& arguments)
return EXIT_SUCCESS;
}
/**
* Read optional password from stdin.
*
* @return Pointer to the PasswordKey or null if passwordkey is skipped
* by user
*/
QSharedPointer<PasswordKey> Create::getPasswordFromStdin()
{
QSharedPointer<PasswordKey> passwordKey;
QTextStream out(Utils::STDOUT, QIODevice::WriteOnly);
out << QObject::tr("Insert password to encrypt database (Press enter to leave blank): ");
out.flush();
QString password = Utils::getPassword();
if (!password.isEmpty()) {
passwordKey = QSharedPointer<PasswordKey>(new PasswordKey(password));
}
return passwordKey;
}
/**
* 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

View File

@ -21,7 +21,6 @@
#include "Command.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
class Create : public Command
{
@ -30,7 +29,6 @@ public:
int execute(const QStringList& arguments) override;
private:
QSharedPointer<PasswordKey> getPasswordFromStdin();
bool loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey);
};

100
src/cli/Import.cpp Normal file
View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <cstdlib>
#include <stdio.h>
#include <QFileInfo>
#include <QString>
#include <QTextStream>
#include "Import.h"
#include "cli/TextStream.h"
#include "cli/Utils.h"
#include "core/Database.h"
#include "keys/CompositeKey.h"
#include "keys/Key.h"
/**
* Create a database file from an XML export of another database.
* A password can be specified to encrypt the database.
* If none is specified the function will fail.
*
* If the database is being saved in a non existant directory, the
* function will fail.
*
* @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("")});
}
int Import::execute(const QStringList& arguments)
{
QSharedPointer<QCommandLineParser> parser = getCommandLineParser(arguments);
if (parser.isNull()) {
return EXIT_FAILURE;
}
TextStream outputTextStream(parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT,
QIODevice::WriteOnly);
TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly);
const QStringList args = parser->positionalArguments();
const QString xmlExportPath = args.at(0);
const QString dbPath = args.at(1);
if (QFileInfo::exists(dbPath)) {
errorTextStream << QObject::tr("File %1 already exists.").arg(dbPath) << endl;
return EXIT_FAILURE;
}
auto key = QSharedPointer<CompositeKey>::create();
auto password = Utils::getPasswordFromStdin();
if (!password.isNull()) {
key->addKey(password);
}
if (key->isEmpty()) {
errorTextStream << QObject::tr("No key is set. Aborting database creation.") << endl;
return EXIT_FAILURE;
}
QString errorMessage;
Database db;
db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2));
db.setKey(key);
if (!db.import(xmlExportPath, &errorMessage)) {
errorTextStream << QObject::tr("Unable to import XML database export %1").arg(errorMessage) << endl;
return EXIT_FAILURE;
}
if (!db.save(dbPath, &errorMessage, true, false)) {
errorTextStream << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl;
return EXIT_FAILURE;
}
outputTextStream << QObject::tr("Successfully imported database.") << endl;
return EXIT_SUCCESS;
}

30
src/cli/Import.h Normal file
View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_IMPORT_H
#define KEEPASSXC_IMPORT_H
#include "Command.h"
class Import : public Command
{
public:
Import();
int execute(const QStringList& arguments) override;
};
#endif // KEEPASSXC_IMPORT_H

View File

@ -127,7 +127,7 @@ namespace Utils
}
if (isPasswordProtected) {
out << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename) << flush;
out << QObject::tr("Enter password to unlock %1: ").arg(databaseFilename) << flush;
QString line = Utils::getPassword(outputDescriptor);
auto passwordKey = QSharedPointer<PasswordKey>::create();
passwordKey->setPassword(line);
@ -217,6 +217,28 @@ namespace Utils
return line;
}
/**
* Read optional password from stdin.
*
* @return Pointer to the PasswordKey or null if passwordkey is skipped
* by user
*/
QSharedPointer<PasswordKey> getPasswordFromStdin()
{
QSharedPointer<PasswordKey> passwordKey;
QTextStream out(Utils::STDOUT, QIODevice::WriteOnly);
out << QObject::tr("Enter password to encrypt database (optional): ");
out.flush();
QString password = Utils::getPassword();
if (!password.isEmpty()) {
passwordKey = QSharedPointer<PasswordKey>(new PasswordKey(password));
}
return passwordKey;
}
/**
* A valid and running event loop is needed to use the global QClipboard,
* so we need to use this from the CLI.

View File

@ -40,6 +40,7 @@ namespace Utils
void setStdinEcho(bool enable);
QString getPassword(FILE* outputDescriptor = STDOUT);
QSharedPointer<PasswordKey> getPasswordFromStdin();
int clipText(const QString& text);
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
const bool isPasswordProtected = true,

View File

@ -51,6 +51,9 @@ Generates a random password.
.IP "help [command]"
Displays a list of available commands, or detailed information about the specified command.
.IP "import [options] <xml> <database>"
Imports the contents of an XML database to the target database.
.IP "locate [options] <database> <term>"
Locates all the entries that match a specific search term in a database.

View File

@ -22,6 +22,7 @@
#include "core/Group.h"
#include "core/Merger.h"
#include "core/Metadata.h"
#include "format/KdbxXmlReader.h"
#include "format/KeePass2Reader.h"
#include "format/KeePass2Writer.h"
#include "keys/FileKey.h"
@ -332,6 +333,24 @@ bool Database::extract(QByteArray& xmlOutput, QString* error)
return true;
}
bool Database::import(const QString& xmlExportPath, QString* error)
{
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
QFile file(xmlExportPath);
file.open(QIODevice::ReadOnly);
reader.readDatabase(&file, this);
if (reader.hasError()) {
if (error) {
*error = reader.errorString();
}
return false;
}
return true;
}
/**
* Remove the old backup and replace it with a new one
* backups are named <filename>.old.<extension>

View File

@ -73,6 +73,7 @@ public:
bool save(QString* error = nullptr, bool atomic = true, bool backup = false);
bool save(const QString& filePath, QString* error = nullptr, bool atomic = true, bool backup = false);
bool extract(QByteArray&, QString* error = nullptr);
bool import(const QString& xmlExportPath, QString* error = nullptr);
bool isInitialized() const;
void setInitialized(bool initialized);

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@ private slots:
void testExport();
void testGenerate_data();
void testGenerate();
void testImport();
void testKeyFileOption();
void testNoPasswordOption();
void testHelp();
@ -77,11 +78,13 @@ private slots:
private:
QByteArray m_dbData;
QByteArray m_dbData2;
QByteArray m_xmlData;
QByteArray m_yubiKeyProtectedDbData;
QByteArray m_keyFileProtectedDbData;
QByteArray m_keyFileProtectedNoPasswordDbData;
QScopedPointer<TemporaryFile> m_dbFile;
QScopedPointer<TemporaryFile> m_dbFile2;
QScopedPointer<TemporaryFile> m_xmlFile;
QScopedPointer<TemporaryFile> m_keyFileProtectedDbFile;
QScopedPointer<TemporaryFile> m_keyFileProtectedNoPasswordDbFile;
QScopedPointer<TemporaryFile> m_yubiKeyProtectedDbFile;