From a2e82dc88395faa73dcf33471637c9465b6ae336 Mon Sep 17 00:00:00 2001 From: louib Date: Fri, 19 May 2017 14:04:11 -0400 Subject: [PATCH] Feature : clip command (#578) --- src/cli/CMakeLists.txt | 2 ++ src/cli/Clip.cpp | 73 +++++++++++++++++++++++++++++++++++++++ src/cli/Clip.h | 27 +++++++++++++++ src/cli/Extract.cpp | 9 +++-- src/cli/List.cpp | 11 +++--- src/cli/Merge.cpp | 29 ++++++++-------- src/cli/Show.cpp | 15 ++++---- src/cli/keepassxc-cli.cpp | 10 ++++-- src/core/Database.cpp | 2 +- src/core/Group.cpp | 46 ++++++++++++++++++++++-- src/core/Group.h | 4 ++- src/core/Uuid.cpp | 7 ++++ src/core/Uuid.h | 3 ++ tests/TestGroup.cpp | 52 ++++++++++++++++++++++++++++ tests/TestGroup.h | 1 + 15 files changed, 250 insertions(+), 41 deletions(-) create mode 100644 src/cli/Clip.cpp create mode 100644 src/cli/Clip.h diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index e090ad1d8..86edb9c01 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -14,6 +14,8 @@ # along with this program. If not, see . set(cli_SOURCES + Clip.cpp + Clip.h EntropyMeter.cpp EntropyMeter.h Extract.cpp diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp new file mode 100644 index 000000000..b3d4fd107 --- /dev/null +++ b/src/cli/Clip.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 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 "Clip.h" + +#include +#include +#include +#include +#include + +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "gui/Clipboard.h" +#include "keys/CompositeKey.h" + +int Clip::execute(int argc, char** argv) +{ + QApplication app(argc, argv); + QTextStream out(stdout); + + QCommandLineParser parser; + parser.setApplicationDescription(QCoreApplication::translate("main", "Copy a password to the clipboard")); + parser.addPositionalArgument("database", QCoreApplication::translate("main", "Path of the database.")); + parser.addPositionalArgument("entry", QCoreApplication::translate("main", "Name of the entry to clip.")); + parser.process(app); + + const QStringList args = parser.positionalArguments(); + if (args.size() != 2) { + parser.showHelp(); + return EXIT_FAILURE; + } + + out << "Insert the database password\n> "; + out.flush(); + + static QTextStream inputTextStream(stdin, QIODevice::ReadOnly); + QString line = inputTextStream.readLine(); + CompositeKey key = CompositeKey::readFromLine(line); + + Database* db = Database::openDatabaseFile(args.at(0), key); + if (!db) { + return EXIT_FAILURE; + } + + QString entryId = args.at(1); + Entry* entry = db->rootGroup()->findEntry(entryId); + if (!entry) { + qCritical("Entry %s not found.", qPrintable(entryId)); + return EXIT_FAILURE; + } + + Clipboard::instance()->setText(entry->password()); + return EXIT_SUCCESS; +} diff --git a/src/cli/Clip.h b/src/cli/Clip.h new file mode 100644 index 000000000..944184095 --- /dev/null +++ b/src/cli/Clip.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2017 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_CLIP_H +#define KEEPASSXC_CLIP_H + +class Clip +{ +public: + static int execute(int argc, char** argv); +}; + +#endif // KEEPASSXC_CLIP_H diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index 81a9ddf07..74fa33da7 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -30,14 +30,14 @@ #include "format/KeePass2Reader.h" #include "keys/CompositeKey.h" -int Extract::execute(int argc, char **argv) +int Extract::execute(int argc, char** argv) { QCoreApplication app(argc, argv); QTextStream out(stdout); QCommandLineParser parser; - parser.setApplicationDescription(QCoreApplication::translate("main", - "Extract and print the content of a database.")); + parser.setApplicationDescription( + QCoreApplication::translate("main", "Extract and print the content of a database.")); parser.addPositionalArgument("database", QCoreApplication::translate("main", "Path of the database to extract.")); parser.process(app); @@ -75,8 +75,7 @@ int Extract::execute(int argc, char **argv) if (reader.hasError()) { if (xmlData.isEmpty()) { qCritical("Error while reading the database:\n%s", qPrintable(reader.errorString())); - } - else { + } else { qWarning("Error while parsing the database:\n%s\n", qPrintable(reader.errorString())); } return EXIT_FAILURE; diff --git a/src/cli/List.cpp b/src/cli/List.cpp index 1e3488106..af3bf0c6d 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -30,7 +30,8 @@ #include "core/Group.h" #include "keys/CompositeKey.h" -void printGroup(Group* group, QString baseName, int depth) { +void printGroup(Group* group, QString baseName, int depth) +{ QTextStream out(stdout); @@ -46,23 +47,21 @@ void printGroup(Group* group, QString baseName, int depth) { } for (Entry* entry : group->entries()) { - out << indentation << " " << entry->title() << " " << entry->uuid().toHex() << "\n"; + out << indentation << " " << entry->title() << " " << entry->uuid().toHex() << "\n"; } for (Group* innerGroup : group->children()) { printGroup(innerGroup, groupName, depth + 1); } - } -int List::execute(int argc, char **argv) +int List::execute(int argc, char** argv) { QCoreApplication app(argc, argv); QTextStream out(stdout); QCommandLineParser parser; - parser.setApplicationDescription(QCoreApplication::translate("main", - "List database entries.")); + parser.setApplicationDescription(QCoreApplication::translate("main", "List database entries.")); parser.addPositionalArgument("database", QCoreApplication::translate("main", "Path of the database.")); parser.process(app); diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index aa399dd5b..8ff8a7b20 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -37,11 +37,15 @@ int Merge::execute(int argc, char** argv) QCommandLineParser parser; parser.setApplicationDescription(QCoreApplication::translate("main", "Merge two databases.")); - parser.addPositionalArgument("database1", QCoreApplication::translate("main", "Path of the database to merge into.")); - parser.addPositionalArgument("database2", QCoreApplication::translate("main", "Path of the database to merge from.")); + parser.addPositionalArgument("database1", + QCoreApplication::translate("main", "Path of the database to merge into.")); + parser.addPositionalArgument("database2", + QCoreApplication::translate("main", "Path of the database to merge from.")); - QCommandLineOption samePasswordOption(QStringList() << "s" << "same-password", - QCoreApplication::translate("main", "Use the same password for both database files.")); + QCommandLineOption samePasswordOption( + QStringList() << "s" + << "same-password", + QCoreApplication::translate("main", "Use the same password for both database files.")); parser.addOption(samePasswordOption); parser.process(app); @@ -54,22 +58,20 @@ int Merge::execute(int argc, char** argv) out << "Insert the first database password\n> "; out.flush(); - + static QTextStream inputTextStream(stdin, QIODevice::ReadOnly); QString line1 = inputTextStream.readLine(); CompositeKey key1 = CompositeKey::readFromLine(line1); CompositeKey key2; if (parser.isSet("same-password")) { - key2 = *key1.clone(); + key2 = *key1.clone(); + } else { + out << "Insert the second database password\n> "; + out.flush(); + QString line2 = inputTextStream.readLine(); + key2 = CompositeKey::readFromLine(line2); } - else { - out << "Insert the second database password\n> "; - out.flush(); - QString line2 = inputTextStream.readLine(); - key2 = CompositeKey::readFromLine(line2); - } - Database* db1 = Database::openDatabaseFile(args.at(0), key1); if (db1 == nullptr) { @@ -104,5 +106,4 @@ int Merge::execute(int argc, char** argv) out << "Successfully merged the database files.\n"; return EXIT_SUCCESS; - } diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index 9222a093d..59c0219a2 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -30,16 +30,15 @@ #include "core/Group.h" #include "keys/CompositeKey.h" -int Show::execute(int argc, char **argv) +int Show::execute(int argc, char** argv) { QCoreApplication app(argc, argv); QTextStream out(stdout); QCommandLineParser parser; - parser.setApplicationDescription(QCoreApplication::translate("main", - "Show a password.")); + parser.setApplicationDescription(QCoreApplication::translate("main", "Show a password.")); parser.addPositionalArgument("database", QCoreApplication::translate("main", "Path of the database.")); - parser.addPositionalArgument("uuid", QCoreApplication::translate("main", "Uuid of the entry to show")); + parser.addPositionalArgument("entry", QCoreApplication::translate("main", "Name of the entry to show.")); parser.process(app); const QStringList args = parser.positionalArguments(); @@ -60,10 +59,10 @@ int Show::execute(int argc, char **argv) return EXIT_FAILURE; } - Uuid uuid = Uuid::fromHex(args.at(1)); - Entry* entry = db->resolveEntry(uuid); - if (entry == nullptr) { - qCritical("No entry found with uuid %s", qPrintable(uuid.toHex())); + QString entryId = args.at(1); + Entry* entry = db->rootGroup()->findEntry(entryId); + if (!entry) { + qCritical("Entry %s not found.", qPrintable(entryId)); return EXIT_FAILURE; } diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index b27b7483f..f6b5df8d7 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -35,7 +36,7 @@ #include #endif -int main(int argc, char **argv) +int main(int argc, char** argv) { #ifdef QT_NO_DEBUG Tools::disableCoreDumps(); @@ -53,6 +54,7 @@ int main(int argc, char **argv) QString description("KeePassXC command line interface."); description = description.append(QString("\n\nAvailable commands:")); + description = description.append(QString("\n clip\t\tCopy a password to the clipboard.")); description = description.append(QString("\n extract\tExtract and print the content of a database.")); description = description.append(QString("\n entropy-meter\tCalculate password entropy.")); description = description.append(QString("\n list\t\tList database entries.")); @@ -82,7 +84,10 @@ int main(int argc, char **argv) int exitCode = EXIT_FAILURE; - if (commandName == "entropy-meter") { + if (commandName == "clip") { + argv[0] = const_cast("keepassxc-cli clip"); + exitCode = Clip::execute(argc, argv); + } else if (commandName == "entropy-meter") { argv[0] = const_cast("keepassxc-cli entropy-meter"); exitCode = EntropyMeter::execute(argc, argv); } else if (commandName == "extract") { @@ -110,5 +115,4 @@ int main(int argc, char **argv) #endif return exitCode; - } diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 5b5a707f9..b3897efae 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -367,7 +367,7 @@ void Database::startModifiedTimer() m_timer->start(150); } -const CompositeKey & Database::key() const +const CompositeKey& Database::key() const { return m_data.key; } diff --git a/src/core/Group.cpp b/src/core/Group.cpp index bd4f8851b..886e55cae 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -483,7 +483,24 @@ QList Group::entriesRecursive(bool includeHistoryItems) const return entryList; } -Entry* Group::findEntry(const Uuid& uuid) +Entry* Group::findEntry(QString entryId) +{ + Q_ASSERT(!entryId.isEmpty()); + Q_ASSERT(!entryId.isNull()); + + if (Uuid::isUuid(entryId)) { + Uuid entryUuid = Uuid::fromHex(entryId); + for (Entry* entry : entriesRecursive(false)) { + if (entry->uuid() == entryUuid) { + return entry; + } + } + } + + return findEntryByPath(entryId); +} + +Entry* Group::findEntryByUuid(const Uuid& uuid) { Q_ASSERT(!uuid.isNull()); for (Entry* entry : asConst(m_entries)) { @@ -495,6 +512,29 @@ Entry* Group::findEntry(const Uuid& uuid) return nullptr; } +Entry* Group::findEntryByPath(QString entryPath, QString basePath) +{ + + Q_ASSERT(!entryPath.isEmpty()); + Q_ASSERT(!entryPath.isNull()); + + for (Entry* entry : asConst(m_entries)) { + QString currentEntryPath = basePath + entry->title(); + if (entryPath == currentEntryPath) { + return entry; + } + } + + for (Group* group : asConst(m_children)) { + Entry* entry = group->findEntryByPath(entryPath, basePath + group->name() + QString("/")); + if (entry != nullptr) { + return entry; + } + } + + return nullptr; +} + QList Group::groupsRecursive(bool includeSelf) const { QList groupList; @@ -551,10 +591,10 @@ void Group::merge(const Group* other) const QList dbEntries = other->entries(); for (Entry* entry : dbEntries) { // entries are searched by uuid - if (!findEntry(entry->uuid())) { + if (!findEntryByUuid(entry->uuid())) { entry->clone(Entry::CloneNoFlags)->setGroup(this); } else { - resolveConflict(findEntry(entry->uuid()), entry); + resolveConflict(findEntryByUuid(entry->uuid()), entry); } } diff --git a/src/core/Group.h b/src/core/Group.h index e3e5e7554..a1b2bcb46 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -78,7 +78,9 @@ public: static const int DefaultIconNumber; static const int RecycleBinIconNumber; - Entry* findEntry(const Uuid& uuid); + Entry* findEntry(QString entryId); + Entry* findEntryByUuid(const Uuid& uuid); + Entry* findEntryByPath(QString entryPath, QString basePath = QString("")); Group* findChildByName(const QString& name); void setUuid(const Uuid& uuid); void setName(const QString& name); diff --git a/src/core/Uuid.cpp b/src/core/Uuid.cpp index 1b046c5a3..1b531159f 100644 --- a/src/core/Uuid.cpp +++ b/src/core/Uuid.cpp @@ -22,6 +22,8 @@ #include "crypto/Random.h" const int Uuid::Length = 16; +const QRegExp Uuid::HexRegExp = QRegExp(QString("^[0-9A-F]{%1}$").arg(QString::number(Uuid::Length * 2)), + Qt::CaseInsensitive); Uuid::Uuid() : m_data(Length, 0) @@ -115,3 +117,8 @@ QDataStream& operator>>(QDataStream& stream, Uuid& uuid) return stream; } + +bool Uuid::isUuid(const QString& uuid) +{ + return Uuid::HexRegExp.exactMatch(uuid); +} diff --git a/src/core/Uuid.h b/src/core/Uuid.h index 4164399d6..ecb20e0c3 100644 --- a/src/core/Uuid.h +++ b/src/core/Uuid.h @@ -20,6 +20,7 @@ #include #include +#include class Uuid { @@ -36,8 +37,10 @@ public: bool operator==(const Uuid& other) const; bool operator!=(const Uuid& other) const; static const int Length; + static const QRegExp HexRegExp; static Uuid fromBase64(const QString& str); static Uuid fromHex(const QString& str); + static bool isUuid(const QString& str); private: QByteArray m_data; diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index e87e6cedc..425bc75c7 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -567,3 +567,55 @@ Database* TestGroup::createMergeTestDatabase() return db; } + +void TestGroup::testFindEntry() +{ + Database* db = new Database(); + + Entry* entry1 = new Entry(); + entry1->setTitle(QString("entry1")); + entry1->setGroup(db->rootGroup()); + entry1->setUuid(Uuid::random()); + + Group* group1 = new Group(); + group1->setName("group1"); + + Entry* entry2 = new Entry(); + + entry2->setTitle(QString("entry2")); + entry2->setGroup(group1); + entry2->setUuid(Uuid::random()); + + group1->setParent(db->rootGroup()); + + Entry* entry; + + entry = db->rootGroup()->findEntry(entry1->uuid().toHex()); + QVERIFY(entry != nullptr); + QCOMPARE(entry->title(), QString("entry1")); + + entry = db->rootGroup()->findEntry(QString("entry1")); + QVERIFY(entry != nullptr); + QCOMPARE(entry->title(), QString("entry1")); + + entry = db->rootGroup()->findEntry(entry2->uuid().toHex()); + QVERIFY(entry != nullptr); + QCOMPARE(entry->title(), QString("entry2")); + + entry = db->rootGroup()->findEntry(QString("group1/entry2")); + QVERIFY(entry != nullptr); + QCOMPARE(entry->title(), QString("entry2")); + + entry = db->rootGroup()->findEntry(QString("invalid/path/to/entry2")); + QVERIFY(entry == nullptr); + + // A valid UUID that does not exist in this database. + entry = db->rootGroup()->findEntry(QString("febfb01ebcdf9dbd90a3f1579dc75281")); + QVERIFY(entry == nullptr); + + // An invalid UUID. + entry = db->rootGroup()->findEntry(QString("febfb01ebcdf9dbd90a3f1579dc")); + QVERIFY(entry == nullptr); + + delete db; +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index c9ed8f087..87795dea2 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -38,6 +38,7 @@ private slots: void testMergeConflict(); void testMergeDatabase(); void testMergeConflictKeepBoth(); + void testFindEntry(); private: Database* createMergeTestDatabase();