diff --git a/COPYING b/COPYING index 9d430b7e93..453b8ab605 100644 --- a/COPYING +++ b/COPYING @@ -182,6 +182,8 @@ Files: share/icons/application/scalable/categories/preferences-other.svg share/icons/application/scalable/actions/document-save-as.svg share/icons/application/scalable/actions/refresh.svg share/icons/application/scalable/actions/clipboard-text.svg + share/icons/application/scalable/actions/reports.svg + share/icons/application/scalable/actions/reports-exclude.svg Copyright: 2019 Austin Andrews License: SIL OPEN FONT LICENSE Version 1.1 Comment: Taken from Material Design icon set (https://github.com/templarian/MaterialDesign/) diff --git a/share/demo.kdbx b/share/demo.kdbx index 1f2cb14727..736fe7544a 100644 Binary files a/share/demo.kdbx and b/share/demo.kdbx differ diff --git a/share/icons/application/scalable/actions/reports-exclude.svg b/share/icons/application/scalable/actions/reports-exclude.svg new file mode 100644 index 0000000000..4418319dc0 --- /dev/null +++ b/share/icons/application/scalable/actions/reports-exclude.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/reports.svg b/share/icons/application/scalable/actions/reports.svg new file mode 100644 index 0000000000..3d62971d2c --- /dev/null +++ b/share/icons/application/scalable/actions/reports.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index 0e1150b6cc..0f3ff6aff6 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -50,7 +50,9 @@ application/scalable/actions/password-generator.svg application/scalable/actions/password-show-off.svg application/scalable/actions/password-show-on.svg - application/scalable/actions/refresh.svg + application/scalable/actions/refresh.svg + application/scalable/actions/reports.svg + application/scalable/actions/reports-exclude.svg application/scalable/actions/sort-alphabetical-ascending.svg application/scalable/actions/sort-alphabetical-descending.svg application/scalable/actions/statistics.svg diff --git a/src/core/PasswordHealth.cpp b/src/core/PasswordHealth.cpp index c179db77ca..bb313170a1 100644 --- a/src/core/PasswordHealth.cpp +++ b/src/core/PasswordHealth.cpp @@ -24,6 +24,9 @@ #include "PasswordHealth.h" #include "zxcvbn.h" +// Define the static member variable with the custom field name +const QString PasswordHealth::OPTION_KNOWN_BAD = QStringLiteral("KnownBad"); + PasswordHealth::PasswordHealth(double entropy) : m_score(entropy) , m_entropy(entropy) diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h index 70f83eee70..ef32493809 100644 --- a/src/core/PasswordHealth.h +++ b/src/core/PasswordHealth.h @@ -83,6 +83,14 @@ class PasswordHealth return m_entropy; } + /** + * Name of custom data field that holds the "this is a known + * bad password" flag. Legal values of the field are TRUE_STR + * and FALSE_STR, the default (used if the field doesn't exist) + * is false. + */ + static const QString OPTION_KNOWN_BAD; + private: int m_score = 0; double m_entropy = 0.0; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 0b3bb04aac..f20f3b9d11 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -355,7 +355,7 @@ MainWindow::MainWindow() m_ui->actionDatabaseSave->setIcon(resources()->icon("document-save")); m_ui->actionDatabaseSaveAs->setIcon(resources()->icon("document-save-as")); m_ui->actionDatabaseClose->setIcon(resources()->icon("document-close")); - m_ui->actionReports->setIcon(resources()->icon("help-about")); + m_ui->actionReports->setIcon(resources()->icon("reports")); m_ui->actionChangeDatabaseSettings->setIcon(resources()->icon("document-edit")); m_ui->actionChangeMasterKey->setIcon(resources()->icon("database-change-key")); m_ui->actionLockDatabases->setIcon(resources()->icon("database-lock")); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 90b49b3da6..8f81d42cc6 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -42,6 +42,7 @@ #include "core/Database.h" #include "core/Entry.h" #include "core/Metadata.h" +#include "core/PasswordHealth.h" #include "core/Resources.h" #include "core/TimeDelta.h" #include "core/Tools.h" @@ -423,6 +424,7 @@ void EditEntryWidget::setupEntryUpdate() // Advanced tab connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified())); connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified())); + connect(m_advancedUi->knownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified())); connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified())); connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified())); connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified())); @@ -827,6 +829,9 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) editTriggers = QAbstractItemView::DoubleClicked; } m_advancedUi->attributesView->setEditTriggers(editTriggers); + m_advancedUi->knownBadCheckBox->setChecked(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) + == TRUE_STR); setupColorButton(true, entry->foregroundColor()); setupColorButton(false, entry->backgroundColor()); m_iconsWidget->setEnabled(!m_history); @@ -1031,6 +1036,13 @@ void EditEntryWidget::updateEntryData(Entry* entry) const entry->setNotes(m_mainUi->notesEdit->toPlainText()); + const auto wasKnownBad = entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR; + const auto isKnownBad = m_advancedUi->knownBadCheckBox->isChecked(); + if (isKnownBad != wasKnownBad) { + entry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR); + } + if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) { entry->setForegroundColor(m_advancedUi->fgColorButton->property("color").toString()); } else { diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui index 7b079b676e..8faa7a4f37 100644 --- a/src/gui/entry/EditEntryWidgetAdvanced.ui +++ b/src/gui/entry/EditEntryWidgetAdvanced.ui @@ -7,7 +7,7 @@ 0 0 532 - 374 + 469 @@ -174,9 +174,31 @@ + + + + <html><head/><body><p>If checked, the entry will not appear in reports like Health Check and HIBP even if it doesn't match the quality requirements (e. g. password entropy or re-use). You can set the check mark if the password is beyond your control (e. g. if it needs to be a four-digit PIN) to prevent it from cluttering the reports.</p></body></html> + + + Exclude from database reports + + + + + 0 + + + 0 + + + 0 + + + 0 + @@ -293,6 +315,7 @@ editAttributeButton protectAttributeButton revealAttributeButton + knownBadCheckBox fgColorCheckBox fgColorButton bgColorCheckBox diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 6fa8e78a6b..00194e182d 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -20,11 +20,13 @@ #include "core/AsyncTask.h" #include "core/Database.h" +#include "core/Global.h" #include "core/Group.h" #include "core/PasswordHealth.h" #include "core/Resources.h" #include "gui/styles/StateColorPalette.h" +#include #include #include @@ -38,11 +40,14 @@ namespace QPointer group; QPointer entry; QSharedPointer health; + bool knownBad = false; Item(const Group* g, const Entry* e, QSharedPointer h) : group(g) , entry(e) , health(h) + , knownBad(e->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && e->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) { } @@ -59,10 +64,16 @@ namespace return m_items; } + bool anyKnownBad() const + { + return m_anyKnownBad; + } + private: QSharedPointer m_db; HealthChecker m_checker; QList> m_items; + bool m_anyKnownBad = false; }; } // namespace @@ -86,8 +97,13 @@ Health::Health(QSharedPointer db) continue; } - // Add entry if its password isn't at least "good" + // Evaluate this entry const auto item = QSharedPointer(new Item(group, entry, m_checker.evaluate(entry))); + if (item->knownBad) { + m_anyKnownBad = true; + } + + // Add entry if its password isn't at least "good" if (item->health->quality() < PasswordHealth::Quality::Good) { m_items.append(item); } @@ -110,8 +126,10 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) m_ui->healthcheckTableView->setModel(m_referencesModel.data()); m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection); m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint))); connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); + connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth())); } ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() @@ -120,7 +138,8 @@ ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer health, const Group* group, - const Entry* entry) + const Entry* entry, + bool knownBad) { QString descr, tip; QColor qualityColor; @@ -151,9 +170,14 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt break; } + auto title = entry->title(); + if (knownBad) { + title.append(tr(" (Excluded)")); + } + auto row = QList(); row << new QStandardItem(descr); - row << new QStandardItem(entry->iconPixmap(), entry->title()); + row << new QStandardItem(entry->iconPixmap(), title); row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/")); row << new QStandardItem(QString::number(health->score())); row << new QStandardItem(health->scoreReason()); @@ -167,6 +191,9 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt // Set tooltips row[0]->setToolTip(tip); + if (knownBad) { + row[1]->setToolTip(tr("This entry is being excluded from reports")); + } row[4]->setToolTip(health->scoreDetails()); // Store entry pointer per table row (used in double click handler) @@ -201,21 +228,41 @@ void ReportsWidgetHealthcheck::calculateHealth() { m_referencesModel->clear(); + // Perform the health check const QScopedPointer health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); })); - if (health->items().empty()) { - // No findings - m_referencesModel->clear(); + + // Display entries that are marked as "known bad"? + const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked(); + + // Display the entries + m_rowToEntry.clear(); + for (const auto& item : health->items()) { + if (item->knownBad && !showKnownBad) { + // Exclude this entry from the report + continue; + } + + // Show the entry in the report + addHealthRow(item->health, item->group, item->entry, item->knownBad); + } + + // Set the table header + if (m_referencesModel->rowCount() == 0) { m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, everything is healthy!")); } else { - // Show our findings m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score") << tr("Reason")); - for (const auto& item : health->items()) { - addHealthRow(item->health, item->group, item->entry); - } } m_ui->healthcheckTableView->resizeRowsToContents(); + + // Show the "show known bad entries" checkbox if there's any known + // bad entry in the database. + if (health->anyKnownBad()) { + m_ui->showKnownBadCheckBox->show(); + } else { + m_ui->showKnownBadCheckBox->hide(); + } } void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) @@ -232,6 +279,57 @@ void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) } } +void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) +{ + + // Find which entry has been clicked + const auto index = m_ui->healthcheckTableView->indexAt(pos); + if (!index.isValid()) { + return; + } + m_contextmenuEntry = const_cast(m_rowToEntry[index.row()].second); + if (!m_contextmenuEntry) { + return; + } + + // Create the context menu + const auto menu = new QMenu(this); + + // Create the "edit entry" menu item + const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this); + menu->addAction(edit); + connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu())); + + // Create the "exclude from reports" menu item + const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this); + knownbad->setCheckable(true); + knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR); + menu->addAction(knownbad); + connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool))); + + // Show the context menu + menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos)); +} + +void ReportsWidgetHealthcheck::editFromContextmenu() +{ + if (m_contextmenuEntry) { + emit entryActivated(m_contextmenuEntry); + } +} + +void ReportsWidgetHealthcheck::toggleKnownBad(bool isKnownBad) +{ + if (!m_contextmenuEntry) { + return; + } + + m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR); + + calculateHealth(); +} + void ReportsWidgetHealthcheck::saveSettings() { // nothing to do - the tab is passive diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index 86931c9dba..ca848e686c 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -54,9 +54,12 @@ class ReportsWidgetHealthcheck : public QWidget public slots: void calculateHealth(); void emitEntryActivated(const QModelIndex& index); + void customMenuRequested(QPoint); + void editFromContextmenu(); + void toggleKnownBad(bool); private: - void addHealthRow(QSharedPointer, const Group*, const Entry*); + void addHealthRow(QSharedPointer, const Group*, const Entry*, bool knownBad); QScopedPointer m_ui; @@ -65,6 +68,7 @@ public slots: QScopedPointer m_referencesModel; QSharedPointer m_db; QList> m_rowToEntry; + Entry* m_contextmenuEntry = nullptr; }; #endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui index 48d8df07fa..202ca6b192 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.ui +++ b/src/gui/reports/ReportsWidgetHealthcheck.ui @@ -6,11 +6,11 @@ 0 0 - 327 + 505 379 - + 0 @@ -24,52 +24,53 @@ 0 - - - Health Check + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + Qt::ElideMiddle + + + false + + + false + + + true + + + false + + + + + + + Also show entries that have been excluded from reports + + + + + + + + true + + + + Hover over reason to show additional details. Double-click entries to edit. - - - - - QAbstractItemView::NoEditTriggers - - - false - - - true - - - Qt::ElideMiddle - - - false - - - true - - - true - - - false - - - - - - - - true - - - - Hover over reason to show additional details. Double-click entries to edit. - - - - diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index d4c3e447b5..3678c29329 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -20,11 +20,32 @@ #include "config-keepassx.h" #include "core/Database.h" +#include "core/Global.h" #include "core/Group.h" +#include "core/PasswordHealth.h" +#include "core/Resources.h" #include "gui/MessageBox.h" +#include #include +namespace +{ + /* + * Check if an entry has been marked as "known bad password". + * These entries are to be excluded from the HIBP report. + * + * Question to reviewer: Should this be a member function of Entry? + * It's duplicated in EditEntryWidget::setForms, EditEntryWidget::updateEntryData, + * ReportsWidgetHealthcheck::customMenuRequested, and Health::Item::Item. + */ + bool isKnownBad(const Entry* entry) + { + return entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR; + } +} // namespace + ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) : QWidget(parent) , m_ui(new Ui::ReportsWidgetHibp()) @@ -37,6 +58,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); + connect(m_ui->hibpTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint))); + connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(makeHibpTable())); #ifdef WITH_XC_NETWORKING connect(&m_downloader, SIGNAL(hibpResult(QString, int)), SLOT(addHibpResult(QString, int))); connect(&m_downloader, SIGNAL(fetchFailed(QString)), SLOT(fetchFailed(QString))); @@ -104,18 +127,43 @@ void ReportsWidgetHibp::makeHibpTable() return lhs.second > rhs.second; }); + // Display entries that are marked as "known bad"? + const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked(); + + // The colors for table cells + const auto red = QBrush("red"); + // Build the table + bool anyKnownBad = false; for (const auto& item : items) { const auto entry = item.first; const auto group = entry->group(); const auto count = item.second; + auto title = entry->title(); + + // If the entry is marked as known bad, hide it unless the + // checkbox is set. + bool knownBad = isKnownBad(entry); + if (knownBad) { + anyKnownBad = true; + if (!showKnownBad) { + continue; + } + + title.append(tr(" (Excluded)")); + } auto row = QList(); - row << new QStandardItem(entry->iconPixmap(), entry->title()) + row << new QStandardItem(entry->iconPixmap(), title) << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/")) << new QStandardItem(countToText(count)); + + if (knownBad) { + row[1]->setToolTip(tr("This entry is being excluded from reports")); + } + + row[2]->setForeground(red); m_referencesModel->appendRow(row); - row[2]->setForeground(QBrush(QColor("red"))); // Store entry pointer per table row (used in double click handler) m_rowToEntry.append(entry); @@ -129,6 +177,22 @@ void ReportsWidgetHibp::makeHibpTable() row[0]->setForeground(QBrush(QColor("red"))); } + // If we're done and everything is good, display a motivational message +#ifdef WITH_XC_NETWORKING + if (m_downloader.passwordsRemaining() == 0 && m_pwndPasswords.isEmpty() && m_error.isEmpty()) { + m_referencesModel->clear(); + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, no exposed passwords!")); + } +#endif + + // Show the "show known bad entries" checkbox if there's any known + // bad entry in the database. + if (anyKnownBad) { + m_ui->showKnownBadCheckBox->show(); + } else { + m_ui->showKnownBadCheckBox->hide(); + } + m_ui->hibpTableView->resizeRowsToContents(); m_ui->stackedWidget->setCurrentIndex(1); @@ -176,7 +240,8 @@ void ReportsWidgetHibp::startValidation() { #ifdef WITH_XC_NETWORKING // Collect all passwords in the database (unless recycled, and - // unless empty) and submit them to the downloader. + // unless empty, and unless marked as "known bad") and submit them + // to the downloader. for (const auto* entry : m_db->rootGroup()->entriesRecursive()) { if (!entry->isRecycled() && !entry->password().isEmpty()) { m_downloader.add(entry->password()); @@ -238,6 +303,7 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index) // Found it, invoke entry editor m_editedEntry = entry; m_editedPassword = entry->password(); + m_editedKnownBad = isKnownBad(entry); emit entryActivated(const_cast(entry)); } } @@ -253,8 +319,13 @@ void ReportsWidgetHibp::refreshAfterEdit() return; } - // No need to re-validate if there was no change - if (m_editedEntry->password() == m_editedPassword) { + // No need to re-validate if there was no change that affects + // the HIBP result (i. e., change to the password or to the + // "known bad" flag) + if (m_editedEntry->password() == m_editedPassword && isKnownBad(m_editedEntry) == m_editedKnownBad) { + // Don't go through HIBP but still rebuild the table, the user might + // have edited the entry title. + makeHibpTable(); return; } @@ -270,6 +341,57 @@ void ReportsWidgetHibp::refreshAfterEdit() m_editedEntry = nullptr; } +void ReportsWidgetHibp::customMenuRequested(QPoint pos) +{ + + // Find which entry has been clicked + const auto index = m_ui->hibpTableView->indexAt(pos); + if (!index.isValid()) { + return; + } + m_contextmenuEntry = const_cast(m_rowToEntry[index.row()]); + if (!m_contextmenuEntry) { + return; + } + + // Create the context menu + const auto menu = new QMenu(this); + + // Create the "edit entry" menu item + const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this); + menu->addAction(edit); + connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu())); + + // Create the "exclude from reports" menu item + const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this); + knownbad->setCheckable(true); + knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR); + menu->addAction(knownbad); + connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool))); + + // Show the context menu + menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos)); +} + +void ReportsWidgetHibp::editFromContextmenu() +{ + if (m_contextmenuEntry) { + emit entryActivated(m_contextmenuEntry); + } +} + +void ReportsWidgetHibp::toggleKnownBad(bool isKnownBad) +{ + if (!m_contextmenuEntry) { + return; + } + + m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR); + + makeHibpTable(); +} + void ReportsWidgetHibp::saveSettings() { // nothing to do - the tab is passive diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h index cd84561218..f7d1a754b0 100644 --- a/src/gui/reports/ReportsWidgetHibp.h +++ b/src/gui/reports/ReportsWidgetHibp.h @@ -58,9 +58,12 @@ public slots: void emitEntryActivated(const QModelIndex&); void addHibpResult(const QString&, int); void fetchFailed(const QString& error); + void makeHibpTable(); + void customMenuRequested(QPoint); + void editFromContextmenu(); + void toggleKnownBad(bool); private: - void makeHibpTable(); void startValidation(); static QString countToText(int count); @@ -73,6 +76,8 @@ public slots: QList m_rowToEntry; // List index is table row QPointer m_editedEntry; // The entry we're currently editing QString m_editedPassword; // The old password of the entry we're editing + bool m_editedKnownBad; // The old "known bad" flag of the entry we're editing + Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked #ifdef WITH_XC_NETWORKING HibpDownloader m_downloader; // This performs the actual HIBP online query diff --git a/src/gui/reports/ReportsWidgetHibp.ui b/src/gui/reports/ReportsWidgetHibp.ui index bda5da6fd2..af5931193b 100644 --- a/src/gui/reports/ReportsWidgetHibp.ui +++ b/src/gui/reports/ReportsWidgetHibp.ui @@ -11,176 +11,218 @@ + + 0 + + + 0 + + + 0 + + + 0 + - - - Have I Been Pwned? + + + 1 - - - - - 0 - - - - - 15 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - 450 - 16777215 - - - - CAUTION: This report requires sending information to the Have I Been Pwned online service (https://haveibeenpwned.com). If you proceed, your database passwords will be cryptographically hashed and the first five characters of those hashes will be sent securely to this service. Your database remains secure and cannot be reconstituted from this information. However, the number of passwords you send and your IP address will be exposed to this service. - - - true - - - - - - - - - 0 - - - - - - 275 - 16777215 - - - - Perform Online Analysis - - - true - - - - - - - - - 0 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 450 + 16777215 + + + + CAUTION: This report requires sending information to the Have I Been Pwned online service (https://haveibeenpwned.com). If you proceed, your database passwords will be cryptographically hashed and the first five characters of those hashes will be sent securely to this service. Your database remains secure and cannot be reconstituted from this information. However, the number of passwords you send and your IP address will be exposed to this service. + + + true + + + + + + + + + 0 + + + + + + 275 + 16777215 + + + + Perform Online Analysis + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 0 + - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QAbstractItemView::NoEditTriggers - - - false - - - true - - - Qt::ElideMiddle - - - false - - - true - - - false - - - - + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + Qt::ElideMiddle + + + false + + + false + + + true + + + false + - - - - - - - 450 - 16777215 - - - - This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + + Also show entries that have been excluded from reports + - - - + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 450 + 16777215 + + + + This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp index f5a99b3632..400d82f29a 100644 --- a/src/gui/reports/ReportsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -20,6 +20,7 @@ #include "core/AsyncTask.h" #include "core/Database.h" +#include "core/Global.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/PasswordHealth.h" @@ -43,6 +44,7 @@ namespace int nPwdsShort = 0; // Number of passwords 8 characters or less in size int nPwdsUnique = 0; // Number of unique passwords int nPwdsReused = 0; // Number of non-unique passwords + int nKnownBad = 0; // Number of known bad entries int pwdTotalLen = 0; // Total length of all passwords // Ctor does all the work @@ -138,6 +140,11 @@ namespace ++nPwdsWeak; } + if (entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) + && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) { + ++nKnownBad; + } + pwdTotalLen += pwd.size(); m_passwords[pwd]++; } @@ -235,6 +242,11 @@ void ReportsWidgetStatistics::calculateStats() QString::number(stats->nPwdsWeak), stats->nPwdsWeak > 0, tr("Recommend using long, randomized passwords with a rating of 'good' or 'excellent'.")); + addStatsRow(tr("Entries excluded from reports"), + QString::number(stats->nKnownBad), + stats->nKnownBad > 0, + tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't " + "necessarily a problem but you should keep an eye on them.")); addStatsRow(tr("Average password length"), tr("%1 characters").arg(stats->averagePwdLength()), stats->isAvgPwdTooShort(), diff --git a/src/gui/reports/ReportsWidgetStatistics.ui b/src/gui/reports/ReportsWidgetStatistics.ui index 1f3bf5fea9..4b96dc51a4 100644 --- a/src/gui/reports/ReportsWidgetStatistics.ui +++ b/src/gui/reports/ReportsWidgetStatistics.ui @@ -6,11 +6,11 @@ 0 0 - 327 + 397 379 - + 0 @@ -24,52 +24,43 @@ 0 - - - Statistics + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + Qt::ElideMiddle + + + false + + + false + + + true + + + false + + + + + + + + true + + + + Hover over lines with error icons for further information. - - - - - QAbstractItemView::NoEditTriggers - - - false - - - true - - - Qt::ElideMiddle - - - false - - - false - - - true - - - false - - - - - - - - true - - - - Hover over lines with error icons for further information. - - - - diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 3b97ffde8d..7ce8a0f4a1 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -45,6 +45,7 @@ #include "core/Entry.h" #include "core/Group.h" #include "core/Metadata.h" +#include "core/PasswordHealth.h" #include "core/Tools.h" #include "crypto/Crypto.h" #include "crypto/kdf/AesKdf.h" @@ -442,6 +443,17 @@ void TestGui::testEditEntry() QCOMPARE(entry->historyItems().size(), ++editCount); QVERIFY(!applyButton->isEnabled()); + // Test the "known bad" checkbox + editEntryWidget->setCurrentPage(1); + auto knownBadCheckBox = editEntryWidget->findChild("knownBadCheckBox"); + QVERIFY(knownBadCheckBox); + QCOMPARE(knownBadCheckBox->isChecked(), false); + knownBadCheckBox->setChecked(true); + QTest::mouseClick(applyButton, Qt::LeftButton); + QCOMPARE(entry->historyItems().size(), ++editCount); + QCOMPARE(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD), true); + QCOMPARE(entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD), TRUE_STR); + // Test entry colors (simulate choosing a color) editEntryWidget->setCurrentPage(1); auto fgColor = QString("#FF0000"); diff --git a/utils/makeicons.sh b/utils/makeicons.sh index d4a8848e18..61ee74e4d2 100644 --- a/utils/makeicons.sh +++ b/utils/makeicons.sh @@ -117,6 +117,8 @@ map() { preferences-other) echo file-document-edit-outline ;; preferences-desktop-icons) echo emoticon-happy-outline ;; preferences-system-network-sharing) echo lan ;; + reports) echo lightbulb-on-outline ;; + reports-exclude) echo lightbulb-off-outline ;; security-high) echo shield-outline ;; sort-alphabetical-ascending) echo sort-alphabetical-ascending ;; sort-alphabetical-descending) echo sort-alphabetical-descending ;;