diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c4f66e713..faaaa8e1e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -170,6 +170,8 @@ set(keepassx_SOURCES gui/reports/ReportsPageHibp.cpp gui/reports/ReportsWidgetStatistics.cpp gui/reports/ReportsPageStatistics.cpp + gui/reports/ReportsPage2FA.cpp + gui/reports/ReportsWidget2FA.cpp gui/osutils/OSUtilsBase.cpp gui/osutils/ScreenLockListener.cpp gui/osutils/ScreenLockListenerPrivate.cpp diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp index e1da39839..4dae86338 100644 --- a/src/gui/reports/ReportsDialog.cpp +++ b/src/gui/reports/ReportsDialog.cpp @@ -21,6 +21,8 @@ #include "ReportsPageHealthcheck.h" #include "ReportsPageHibp.h" #include "ReportsPageStatistics.h" +#include "ReportsPage2FA.h" + #ifdef WITH_XC_BROWSER #include "ReportsPageBrowserStatistics.h" #include "ReportsWidgetBrowserStatistics.h" @@ -62,6 +64,7 @@ ReportsDialog::ReportsDialog(QWidget* parent) , m_healthPage(new ReportsPageHealthcheck()) , m_hibpPage(new ReportsPageHibp()) , m_statPage(new ReportsPageStatistics()) + , m_2faPage(new ReportsPage2FA()) #ifdef WITH_XC_BROWSER , m_browserStatPage(new ReportsPageBrowserStatistics()) #endif @@ -76,6 +79,7 @@ ReportsDialog::ReportsDialog(QWidget* parent) #endif addPage(m_healthPage); addPage(m_hibpPage); + addPage(m_2faPage); m_ui->stackedWidget->setCurrentIndex(0); @@ -87,6 +91,8 @@ ReportsDialog::ReportsDialog(QWidget* parent) connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int))); connect(m_healthPage->m_healthWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*))); connect(m_hibpPage->m_hibpWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*))); + connect(m_2faPage->m_2faWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*))); + #ifdef WITH_XC_BROWSER connect(m_browserStatPage->m_browserWidget, SIGNAL(entryActivated(Entry*)), diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h index 25cb623eb..210903817 100644 --- a/src/gui/reports/ReportsDialog.h +++ b/src/gui/reports/ReportsDialog.h @@ -29,6 +29,7 @@ class QTabWidget; class ReportsPageHealthcheck; class ReportsPageHibp; class ReportsPageStatistics; +class ReportsPage2FA; #ifdef WITH_XC_BROWSER class ReportsPageBrowserStatistics; #endif @@ -77,6 +78,7 @@ private: const QSharedPointer m_healthPage; const QSharedPointer m_hibpPage; const QSharedPointer m_statPage; + const QSharedPointer m_2faPage; #ifdef WITH_XC_BROWSER const QSharedPointer m_browserStatPage; #endif diff --git a/src/gui/reports/ReportsPage2FA.cpp b/src/gui/reports/ReportsPage2FA.cpp new file mode 100644 index 000000000..299d6ac14 --- /dev/null +++ b/src/gui/reports/ReportsPage2FA.cpp @@ -0,0 +1,42 @@ +// +// Created by Thomas on 1/04/2022. +// + +#include "ReportsPage2FA.h" +#include "ReportsWidget2FA.h" +#include "gui/Icons.h" + + + +QString ReportsPage2FA::name() +{ + return QObject::tr("2FA"); +} + +QIcon ReportsPage2FA::icon() +{ + return icons()->icon("chronometer"); +} + +QWidget* ReportsPage2FA::createWidget() +{ + return m_2faWidget; +} + +void ReportsPage2FA::loadSettings(QWidget* widget, QSharedPointer db) +{ + ReportsWidget2FA* settingsWidget = reinterpret_cast(widget); + settingsWidget->loadSettings(db); +} + +void ReportsPage2FA::saveSettings(QWidget* widget) +{ + ReportsWidget2FA* settingsWidget = reinterpret_cast(widget); + settingsWidget->saveSettings(); +} + +ReportsPage2FA::ReportsPage2FA() + : m_2faWidget(new ReportsWidget2FA()) +{ + +} diff --git a/src/gui/reports/ReportsPage2FA.h b/src/gui/reports/ReportsPage2FA.h new file mode 100644 index 000000000..55402ccef --- /dev/null +++ b/src/gui/reports/ReportsPage2FA.h @@ -0,0 +1,25 @@ +// +// Created by Thomas on 1/04/2022. +// + +#ifndef KEEPASSXC_REPORTSPAGE2FA_H +#define KEEPASSXC_REPORTSPAGE2FA_H + +#include "ReportsDialog.h" +#include "ReportsWidget2FA.h" + +class ReportsPage2FA : public IReportsPage +{ + + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget, QSharedPointer db) override; + void saveSettings(QWidget* widget) override; + +public: + ReportsWidget2FA* m_2faWidget; + ReportsPage2FA(); +}; + +#endif // KEEPASSXC_REPORTSPAGE2FA_H diff --git a/src/gui/reports/ReportsWidget2FA.cpp b/src/gui/reports/ReportsWidget2FA.cpp new file mode 100644 index 000000000..3889df516 --- /dev/null +++ b/src/gui/reports/ReportsWidget2FA.cpp @@ -0,0 +1,258 @@ +// +// Created by Thomas on 1/04/2022. +// + +#include +#include +#include +#include +#include +#include +#include "ReportsWidget2FA.h" +#include "ui_ReportsWidget2FA.h" +#include "gui/Icons.h" +#include "core/Group.h" +#include "core/NetworkManager.h" + +ReportsWidget2FA::ReportsWidget2FA(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::ReportsWidget2FA) +{ + m_ui->setupUi(this); + + m_ui->downloadingDirectory->hide(); + m_ui->downloadDirectoryError->hide(); + +#ifdef WITH_XC_NETWORKING + m_ui->noNetwork->hide(); +#endif + m_referencesModel.reset(new QStandardItemModel()); + m_ui->mfaTableView->setModel(m_referencesModel.data()); + m_ui->mfaTableView->setSelectionMode(QAbstractItemView::NoSelection); + m_ui->mfaTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + connect(m_ui->enabledFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable())); + connect(m_ui->disabledFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable())); + connect(m_ui->notSupportedFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable())); + connect(m_ui->supportedFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable())); + + connect(m_ui->mfaTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(cellDoubleClick(QModelIndex))); +} +ReportsWidget2FA::~ReportsWidget2FA() +{ + delete m_ui; +} + +void ReportsWidget2FA::saveSettings() +{ + // no settings to save +} + +void ReportsWidget2FA::loadSettings(QSharedPointer db) +{ + m_db = std::move(db); + + m_referencesModel->clear(); + + auto row = QList(); + row << new QStandardItem("Please wait while your database is analysed."); + m_referencesModel->appendRow(row); + +#ifdef WITH_XC_NETWORKING + fetchDirectory(); +#else + make2FATable(false); +#endif +} + +void ReportsWidget2FA::fetchDirectory() +{ + m_ui->downloadingDirectory->show(); + m_ui->downloadDirectoryError->hide(); + + m_bytesReceived.clear(); + + QUrl directoryUrl = QUrl("https://2fa.directory/api/v1/data.json"); + + QNetworkRequest request(directoryUrl); + + m_reply = getNetMgr()->get(request); + + connect(m_reply, &QNetworkReply::finished, this, &ReportsWidget2FA::fetchDirectoryFinished); + connect(m_reply, &QIODevice::readyRead, this, &ReportsWidget2FA::fetchDirectoryReadyRead); +} + +void ReportsWidget2FA::fetchDirectoryReadyRead() +{ + m_bytesReceived += m_reply->readAll(); +} + +void ReportsWidget2FA::fetchDirectoryFinished() +{ + bool error = (m_reply->error() != QNetworkReply::NoError); + + m_reply->deleteLater(); + m_reply = nullptr; + + if(!error){ + QJsonDocument jsonResponse = QJsonDocument::fromJson(m_bytesReceived); + QJsonObject jsonObject = jsonResponse.object(); + + // object is a dictionary of category->entryName->entry + m_2faDirectoryEntries.clear(); + + for(auto category : jsonObject.keys()) { + if(!jsonObject.value(category).isObject()) + goto error; // The schema appears to have changed + + auto categoryEntries = jsonObject.value(category).toObject(); + + for(const auto& categoryEntriesName : categoryEntries.keys()){ + auto jsonEntry = categoryEntries.value(categoryEntriesName); + if(!jsonEntry.isObject()) + goto error; + + auto entry = jsonEntry.toObject(); + + auto entryInstance = new MFADirectoryEntry( + entry.contains("tfa") && entry.value("tfa").toBool(), + entry.value("name").toString(), + entry.value("url").toString(), + category, + entry.value("doc").toString() + ); + + m_2faDirectoryEntries.append(*entryInstance); + } + } + + make2FATable(true); + }else{ + goto error; + } + + m_ui->downloadingDirectory->hide(); + return; + + error: + m_ui->downloadDirectoryError->show(); + make2FATable(false); +} + +void ReportsWidget2FA::make2FATable(bool useDirectory) +{ + m_directoryUsed = useDirectory; + m_referencesModel->clear(); + + if(useDirectory) { + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("2FA Enabled") + << tr("2FA Supported") << tr("Matched Site") + << tr("Documentation")); + + m_ui->supportedFilter->show(); + m_ui->notSupportedFilter->show(); + }else{ + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("2FA Enabled")); + + m_ui->supportedFilter->hide(); + m_ui->notSupportedFilter->hide(); + } + + + for(auto entry : m_db->rootGroup()->entriesRecursive()){ + if(!entry->isRecycled() && !entry->excludeFromReports()){ + auto group = entry->group(); + auto hasMFA = entry->hasTotp(); + MFADirectoryEntry* matchedDirectoryEntry = nullptr; + + auto row = QList(); + row << new QStandardItem(Icons::entryIconPixmap(entry), entry->title()) + << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/")) + << new QStandardItem(hasMFA ? tr("Enabled") : tr("Disabled")); + + if(hasMFA && !m_ui->enabledFilter->isChecked()) continue; + if(!hasMFA && !m_ui->disabledFilter->isChecked()) continue; + + if(useDirectory){ + + for(auto directoryEntry : m_2faDirectoryEntries){ + if(QUrl(entry->url()).host() == QUrl(directoryEntry.url()).host()){ // highly likely this is a match + matchedDirectoryEntry = &directoryEntry; + break; + } + } + + if(matchedDirectoryEntry == nullptr){ + row << new QStandardItem(tr("Unknown")) + << new QStandardItem(tr("No match")); + + if(!m_ui->notSupportedFilter->isChecked()) continue; + }else{ + row << new QStandardItem(matchedDirectoryEntry->supported() ? tr("Supported") : tr("Not Supported")) + << new QStandardItem(QString("%1 (%2)").arg(matchedDirectoryEntry->name(), + matchedDirectoryEntry->url())) + << new QStandardItem(matchedDirectoryEntry->docs()); + + if(!m_ui->supportedFilter->isChecked()) continue; + } + } + + m_referencesModel->appendRow(row); + m_tableData.append(new QPair(entry, matchedDirectoryEntry->docs())); + } + } +} + +void ReportsWidget2FA::refresh2FATable() +{ + make2FATable(m_directoryUsed); +} + +void ReportsWidget2FA::cellDoubleClick(const QModelIndex& index) +{ + if(!index.isValid()){ + return; + } + + auto clickedRow = m_tableData[index.row()]; + if(index.column() == 5) { // docs column + auto directoryUrl = clickedRow->second; + QDesktopServices::openUrl(directoryUrl); + }else{ + emit entryActivated(clickedRow->first); + } +} + +bool MFADirectoryEntry::supported() +{ + return m_2faSupported; +} + +QString MFADirectoryEntry::url() +{ + return m_url; +} + +QString MFADirectoryEntry::name() +{ + return m_name; +} + +QString MFADirectoryEntry::category() +{ + return m_category; +} + +QString MFADirectoryEntry::docs() +{ + return m_docs; +} +MFADirectoryEntry::MFADirectoryEntry(bool supported, QString name, QString url, QString category, QString docs) +{ + m_2faSupported = supported; + m_name = std::move(name); + m_url = std::move(url); + m_category = std::move(category); + m_docs = std::move(docs); +} + diff --git a/src/gui/reports/ReportsWidget2FA.h b/src/gui/reports/ReportsWidget2FA.h new file mode 100644 index 000000000..15b5d2ed5 --- /dev/null +++ b/src/gui/reports/ReportsWidget2FA.h @@ -0,0 +1,83 @@ +// +// Created by Thomas on 1/04/2022. +// + +#ifndef KEEPASSXC_REPORTSWIDGET2FA_H +#define KEEPASSXC_REPORTSWIDGET2FA_H + +#include +#include +#include +#include +#include + +class Database; +class Entry; + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class ReportsWidget2FA; +} +QT_END_NAMESPACE + +class MFADirectoryEntry { +public: + MFADirectoryEntry(bool supported, QString name, QString url, QString category, QString docs); + + bool supported(); + QString url(); + QString name(); + QString category(); + QString docs(); + +private: + bool m_2faSupported; + QString m_url; + QString m_name; + QString m_category; + QString m_docs; + +}; + +class ReportsWidget2FA : public QWidget +{ + Q_OBJECT + +public: + explicit ReportsWidget2FA(QWidget* parent = nullptr); + ~ReportsWidget2FA() override; + + void loadSettings(QSharedPointer db); + void saveSettings(); + +signals: + void entryActivated(Entry*); + +private slots: + void fetchDirectoryFinished(); + void fetchDirectoryReadyRead(); + + void refresh2FATable(); + + void cellDoubleClick(const QModelIndex& index); + + +private: + void make2FATable(bool useDirectory); + + void fetchDirectory(); + + Ui::ReportsWidget2FA* m_ui; + + bool m_directoryUsed = false; + + QByteArray m_bytesReceived; + QList*> m_tableData; + QList m_2faDirectoryEntries; + QScopedPointer m_referencesModel; + QSharedPointer m_db; + QNetworkReply* m_reply; +}; + +#endif // KEEPASSXC_REPORTSWIDGET2FA_H diff --git a/src/gui/reports/ReportsWidget2FA.ui b/src/gui/reports/ReportsWidget2FA.ui new file mode 100644 index 000000000..9b12d2b9b --- /dev/null +++ b/src/gui/reports/ReportsWidget2FA.ui @@ -0,0 +1,137 @@ + + + ReportsWidget2FA + + + + 0 + 0 + 634 + 364 + + + + ReportsWidget2FA + + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + Downloading the 2FA Directory Failed + + + + + + + Downloading 2FA Directory (provided by 2fa.directory) + + + + + + + true + + + This build of KeepassXC doesn't have networking functions. Networking is required to provide 2FA suggestions + + + true + + + + + + + + true + + + + Double-click entries to edit them + + + + + + + + + + + Filter + + + + + + Supported + + + true + + + + + + + Not Supported + + + true + + + + + + + Enabled + + + true + + + + + + + Disabled + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + +