From 891f67a1cdbd88c4ff9d1f61fd82d1aff2a9d9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Wed, 30 Jan 2019 16:48:22 +0200 Subject: [PATCH] Select group when adding credentials from browser extension (#2637) --- src/browser/BrowserAction.cpp | 42 +++++++++++++++++- src/browser/BrowserAction.h | 4 +- src/browser/BrowserService.cpp | 80 ++++++++++++++++++++++++++++++---- src/browser/BrowserService.h | 4 ++ src/core/Group.cpp | 19 ++++++++ src/core/Group.h | 1 + src/core/Tools.cpp | 5 +++ src/core/Tools.h | 1 + tests/TestGroup.cpp | 32 ++++++++++++++ tests/TestGroup.h | 1 + 10 files changed, 178 insertions(+), 11 deletions(-) diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index ed3a40f6d..433bfe2c5 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -85,6 +85,8 @@ QJsonObject BrowserAction::handleAction(const QJsonObject& json) return handleSetLogin(json, action); } else if (action.compare("lock-database", Qt::CaseSensitive) == 0) { return handleLockDatabase(json, action); + } else if (action.compare("get-database-groups", Qt::CaseSensitive) == 0) { + return handleGetDatabaseGroups(json, action); } // Action was not recognized @@ -320,10 +322,12 @@ QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString const QString password = decrypted.value("password").toString(); const QString submitUrl = decrypted.value("submitUrl").toString(); const QString uuid = decrypted.value("uuid").toString(); + const QString group = decrypted.value("group").toString(); + const QString groupUuid = decrypted.value("groupUuid").toString(); const QString realm; if (uuid.isEmpty()) { - m_browserService.addEntry(id, login, password, url, submitUrl, realm); + m_browserService.addEntry(id, login, password, url, submitUrl, realm, group, groupUuid); } else { m_browserService.updateEntry(id, uuid, login, password, url, submitUrl); } @@ -368,6 +372,40 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); } +QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + + QMutexLocker locker(&m_mutex); + if (!m_associated) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + QString command = decrypted.value("action").toString(); + if (command.isEmpty() || command.compare("get-database-groups", Qt::CaseSensitive) != 0) { + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); + } + + const QJsonObject groups = m_browserService.getDatabaseGroups(); + if (groups.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_GROUPS_FOUND); + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["groups"] = groups; + + return buildResponse(action, message, newNonce); +} + QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const { QJsonObject response; @@ -427,6 +465,8 @@ QString BrowserAction::getErrorMessage(const int errorCode) const return QObject::tr("No URL provided"); case ERROR_KEEPASS_NO_LOGINS_FOUND: return QObject::tr("No logins found"); + case ERROR_KEEPASS_NO_GROUPS_FOUND: + return QObject::tr("No groups found"); default: return QObject::tr("Unknown error"); } diff --git a/src/browser/BrowserAction.h b/src/browser/BrowserAction.h index 41a3b13dc..29736ab4e 100644 --- a/src/browser/BrowserAction.h +++ b/src/browser/BrowserAction.h @@ -45,7 +45,8 @@ class BrowserAction : public QObject ERROR_KEEPASS_INCORRECT_ACTION = 12, ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED = 13, ERROR_KEEPASS_NO_URL_PROVIDED = 14, - ERROR_KEEPASS_NO_LOGINS_FOUND = 15 + ERROR_KEEPASS_NO_LOGINS_FOUND = 15, + ERROR_KEEPASS_NO_GROUPS_FOUND = 16 }; public: @@ -64,6 +65,7 @@ private: QJsonObject handleGeneratePassword(const QJsonObject& json, const QString& action); QJsonObject handleSetLogin(const QJsonObject& json, const QString& action); QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action); + QJsonObject handleGetDatabaseGroups(const QJsonObject& json, const QString& action); QJsonObject buildMessage(const QString& nonce) const; QJsonObject buildResponse(const QString& action, const QJsonObject& message, const QString& nonce); diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index e84a1dadb..aa114cef0 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -56,7 +56,7 @@ BrowserService::BrowserService(DatabaseTabWidget* parent) , m_bringToFrontRequested(false) , m_wasMinimized(false) , m_wasHidden(false) - , m_keepassBrowserUUID(QUuid::fromRfc4122(QByteArray::fromHex("de887cc3036343b8974b5911b8816224"))) + , m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224")) { // Don't connect the signals when used from DatabaseSettingsWidgetBrowser (parent is nullptr) if (m_dbTabWidget) { @@ -151,6 +151,54 @@ QString BrowserService::getDatabaseRecycleBinUuid() return recycleBin->uuidToHex(); } +QJsonArray BrowserService::addChildrenToGroup(Group* group) +{ + QJsonArray groupList; + + if (!group) { + return groupList; + } + + for (const auto& c : group->children()) { + if (c == group->database()->metadata()->recycleBin()) { + continue; + } + + QJsonObject jsonGroup; + jsonGroup["name"] = c->name(); + jsonGroup["uuid"] = Tools::uuidToHex(c->uuid()); + jsonGroup["children"] = addChildrenToGroup(c); + groupList.push_back(jsonGroup); + } + return groupList; +} + +QJsonObject BrowserService::getDatabaseGroups() +{ + auto db = getDatabase(); + if (!db) { + return {}; + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return {}; + } + + QJsonObject root; + root["name"] = rootGroup->name(); + root["uuid"] = Tools::uuidToHex(rootGroup->uuid()); + root["children"] = addChildrenToGroup(rootGroup); + + QJsonArray groups; + groups.push_back(root); + + QJsonObject result; + result["groups"] = groups; + + return result; +} + QString BrowserService::storeKey(const QString& key) { QString id; @@ -298,6 +346,8 @@ void BrowserService::addEntry(const QString& id, const QString& url, const QString& submitUrl, const QString& realm, + const QString& group, + const QString& groupUuid, QSharedPointer selectedDb) { if (thread() != QThread::currentThread()) { @@ -310,6 +360,8 @@ void BrowserService::addEntry(const QString& id, Q_ARG(QString, url), Q_ARG(QString, submitUrl), Q_ARG(QString, realm), + Q_ARG(QString, group), + Q_ARG(QString, groupUuid), Q_ARG(QSharedPointer, selectedDb)); } @@ -318,8 +370,8 @@ void BrowserService::addEntry(const QString& id, return; } - Group* group = findCreateAddEntryGroup(db); - if (!group) { + auto* addEntryGroup = findCreateAddEntryGroup(db); + if (!addEntryGroup) { return; } @@ -330,7 +382,17 @@ void BrowserService::addEntry(const QString& id, entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); entry->setUsername(login); entry->setPassword(password); - entry->setGroup(group); + entry->setGroup(addEntryGroup); + + // Select a group for the entry + if (!group.isEmpty()) { + if (db->rootGroup()) { + auto selectedGroup = db->rootGroup()->findGroupByUuid(Tools::hexToUuid(groupUuid)); + if (selectedGroup && selectedGroup->name() == group) { + entry->setGroup(selectedGroup); + } + } + } const QString host = QUrl(url).host(); const QString submitHost = QUrl(submitUrl).host(); @@ -370,10 +432,10 @@ void BrowserService::updateEntry(const QString& id, return; } - Entry* entry = db->rootGroup()->findEntryByUuid(QUuid::fromRfc4122(QByteArray::fromHex(uuid.toLatin1()))); + Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid)); if (!entry) { // If entry is not found for update, add a new one to the selected database - addEntry(id, login, password, url, submitUrl, "", db); + addEntry(id, login, password, url, submitUrl, "", "", "", db); return; } @@ -715,7 +777,7 @@ Group* BrowserService::findCreateAddEntryGroup(QSharedPointer selected return nullptr; } - Group* rootGroup = db->rootGroup(); + auto* rootGroup = db->rootGroup(); if (!rootGroup) { return nullptr; } @@ -723,8 +785,8 @@ Group* BrowserService::findCreateAddEntryGroup(QSharedPointer selected const QString groupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME); // TODO: setting to decide where new keys are created - for (const Group* g : rootGroup->groupsRecursive(true)) { - if (g->name() == groupName) { + for (auto* g : rootGroup->groupsRecursive(true)) { + if (g->name() == groupName && !g->isRecycled()) { return db->rootGroup()->findGroupByUuid(g->uuid()); } } diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 3ec839563..8db7be5d8 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -44,6 +44,7 @@ public: bool openDatabase(bool triggerUnlock); QString getDatabaseRootUuid(); QString getDatabaseRecycleBinUuid(); + QJsonObject getDatabaseGroups(); QString getKey(const QString& id); void addEntry(const QString& id, const QString& login, @@ -51,6 +52,8 @@ public: const QString& url, const QString& submitUrl, const QString& realm, + const QString& group, + const QString& groupUuid, QSharedPointer selectedDb = {}); QList searchEntries(QSharedPointer db, const QString& hostname, const QString& url); QList searchEntries(const QString& url, const StringPairList& keyList); @@ -111,6 +114,7 @@ private: QString baseDomain(const QString& url) const; QSharedPointer getDatabase(); QSharedPointer selectedDatabase(); + QJsonArray addChildrenToGroup(Group* group); bool moveSettingsToCustomData(Entry* entry, const QString& name) const; int moveKeysToCustomData(Entry* entry, QSharedPointer db) const; bool checkLegacySettings(); diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 9cfce863e..fc9726f1d 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -257,6 +257,25 @@ Entry* Group::lastTopVisibleEntry() const return m_lastTopVisibleEntry; } +bool Group::isRecycled() +{ + Group* group = this; + if (!group->database()) { + return false; + } + + do { + if (group->m_parent && group->m_db->metadata()) { + if (group->m_parent == group->m_db->metadata()->recycleBin()) { + return true; + } + } + group = group->m_parent; + } while (group && group->m_parent && group->m_parent != group->m_db->rootGroup()); + + return false; +} + bool Group::isExpired() const { return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc(); diff --git a/src/core/Group.h b/src/core/Group.h index d0074d5d3..4b8569207 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -102,6 +102,7 @@ public: bool resolveAutoTypeEnabled() const; Entry* lastTopVisibleEntry() const; bool isExpired() const; + bool isRecycled(); CustomData* customData(); const CustomData* customData() const; diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index e72901a61..b77a460d4 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -213,6 +213,11 @@ namespace Tools return QString::fromLatin1(uuid.toRfc4122().toHex()); } + QUuid hexToUuid(const QString& uuid) + { + return QUuid::fromRfc4122(QByteArray::fromHex(uuid.toLatin1())); + } + Buffer::Buffer() : raw(nullptr) , size(0) diff --git a/src/core/Tools.h b/src/core/Tools.h index 6d190a466..a2c09efe2 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -41,6 +41,7 @@ namespace Tools void sleep(int ms); void wait(int ms); QString uuidToHex(const QUuid& uuid); + QUuid hexToUuid(const QString& uuid); QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, bool exactMatch = false, diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 7abb808d1..6ea43b105 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -766,3 +766,35 @@ void TestGroup::testAddEntryWithPath() delete db; } + +void TestGroup::testIsRecycled() +{ + Database* db = new Database(); + db->rootGroup()->createRecycleBin(); + + Group* group1 = new Group(); + group1->setName("group1"); + group1->setParent(db->rootGroup()); + + Group* group2 = new Group(); + group2->setName("group2"); + group2->setParent(db->rootGroup()); + + Group* group3 = new Group(); + group3->setName("group3"); + group3->setParent(group2); + + Group* group4 = new Group(); + group4->setName("group4"); + group4->setParent(db->rootGroup()); + + db->recycleGroup(group2); + + QVERIFY(!group1->isRecycled()); + QVERIFY(group2->isRecycled()); + QVERIFY(group3->isRecycled()); + QVERIFY(!group4->isRecycled()); + + db->recycleGroup(group4); + QVERIFY(group4->isRecycled()); +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index c6ccb21f9..fe0fefda4 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -42,6 +42,7 @@ private slots: void testPrint(); void testLocate(); void testAddEntryWithPath(); + void testIsRecycled(); }; #endif // KEEPASSX_TESTGROUP_H