Reports: Add "Known Bad" flag for entries

* Fixes #4168

* Introduce a custom data element stored with an entry to indicate that it is a "Known Bad" entry. This flag causes database reports to skip these entries.
* The current number of known bad entries is displayed in the statistics report.
* Add context menu to reports to easily exclude entries.
This commit is contained in:
Wolfram Rösler 2020-04-03 22:01:00 +02:00 committed by Jonathan White
parent ce8f32e797
commit 3c19fdd193
20 changed files with 620 additions and 279 deletions

View File

@ -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 <http://templarian.com/>
License: SIL OPEN FONT LICENSE Version 1.1
Comment: Taken from Material Design icon set (https://github.com/templarian/MaterialDesign/)

Binary file not shown.

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-lightbulb-off-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2C9.76,2 7.78,3.05 6.5,4.68L7.93,6.11C8.84,4.84 10.32,4 12,4A5,5 0 0,1 17,9C17,10.68 16.16,12.16 14.89,13.06L16.31,14.5C17.94,13.21 19,11.24 19,9A7,7 0 0,0 12,2M3.28,4L2,5.27L5.04,8.3C5,8.53 5,8.76 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H14.73L18.73,22L20,20.72L3.28,4M7.23,10.5L12.73,16H10V13.58C8.68,13 7.66,11.88 7.23,10.5M9,20V21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9Z" /></svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-lightbulb-on-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M11,18H13V15.87C14.73,15.43 16,13.86 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13.86 9.27,15.43 11,15.87V18Z" /></svg>

After

Width:  |  Height:  |  Size: 758 B

View File

@ -50,7 +50,9 @@
<file>application/scalable/actions/password-generator.svg</file>
<file>application/scalable/actions/password-show-off.svg</file>
<file>application/scalable/actions/password-show-on.svg</file>
<file>application/scalable/actions/refresh.svg</file>
<file>application/scalable/actions/refresh.svg</file>
<file>application/scalable/actions/reports.svg</file>
<file>application/scalable/actions/reports-exclude.svg</file>
<file>application/scalable/actions/sort-alphabetical-ascending.svg</file>
<file>application/scalable/actions/sort-alphabetical-descending.svg</file>
<file>application/scalable/actions/statistics.svg</file>

View File

@ -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)

View File

@ -83,6 +83,14 @@ public:
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;

View File

@ -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"));

View File

@ -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 {

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>532</width>
<height>374</height>
<height>469</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -174,9 +174,31 @@
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="knownBadCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Exclude from database reports</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="colorsBox" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="fgColorCheckBox">
<property name="text">
@ -293,6 +315,7 @@
<tabstop>editAttributeButton</tabstop>
<tabstop>protectAttributeButton</tabstop>
<tabstop>revealAttributeButton</tabstop>
<tabstop>knownBadCheckBox</tabstop>
<tabstop>fgColorCheckBox</tabstop>
<tabstop>fgColorButton</tabstop>
<tabstop>bgColorCheckBox</tabstop>

View File

@ -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 <QMenu>
#include <QSharedPointer>
#include <QStandardItemModel>
@ -38,11 +40,14 @@ namespace
QPointer<const Group> group;
QPointer<const Entry> entry;
QSharedPointer<PasswordHealth> health;
bool knownBad = false;
Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> 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<Database> m_db;
HealthChecker m_checker;
QList<QSharedPointer<Item>> m_items;
bool m_anyKnownBad = false;
};
} // namespace
@ -86,8 +97,13 @@ Health::Health(QSharedPointer<Database> db)
continue;
}
// Add entry if its password isn't at least "good"
// Evaluate this entry
const auto item = QSharedPointer<Item>(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<PasswordHealth> 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<PasswordHealth> healt
break;
}
auto title = entry->title();
if (knownBad) {
title.append(tr(" (Excluded)"));
}
auto row = QList<QStandardItem*>();
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<PasswordHealth> 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> 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<Entry*>(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

View File

@ -54,9 +54,12 @@ signals:
public slots:
void calculateHealth();
void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
void editFromContextmenu();
void toggleKnownBad(bool);
private:
void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*);
void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
@ -65,6 +68,7 @@ private:
QScopedPointer<QStandardItemModel> m_referencesModel;
QSharedPointer<Database> m_db;
QList<QPair<const Group*, const Entry*>> m_rowToEntry;
Entry* m_contextmenuEntry = nullptr;
};
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H

View File

@ -6,11 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<width>505</width>
<height>379</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
<property name="leftMargin">
<number>0</number>
</property>
@ -24,52 +24,53 @@
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="healthcheckGroupBox">
<property name="title">
<string>Health Check</string>
<widget class="QTableView" name="healthcheckTableView">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showKnownBadCheckBox">
<property name="text">
<string>Also show entries that have been excluded from reports</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="tipLabel">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Hover over reason to show additional details. Double-click entries to edit.</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="healthcheckTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="tipLabel">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Hover over reason to show additional details. Double-click entries to edit.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>

View File

@ -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 <QMenu>
#include <QStandardItemModel>
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<QStandardItem*>();
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*>(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<Entry*>(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

View File

@ -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 @@ private:
QList<const Entry*> m_rowToEntry; // List index is table row
QPointer<const Entry> 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

View File

@ -11,176 +11,218 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Have I Been Pwned?</string>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>1</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="confirmation">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>15</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>450</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>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.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="validationButton">
<property name="maximumSize">
<size>
<width>275</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Perform Online Analysis</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
<widget class="QWidget" name="confirmation">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>15</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>450</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>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.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="validationButton">
<property name="maximumSize">
<size>
<width>275</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Perform Online Analysis</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
<widget class="QWidget" name="resultsTable">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="hibpTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="resultsTable">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="hibpTableView">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
<widget class="QWidget" name="noNetwork">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="networkNoticeLabel">
<property name="maximumSize">
<size>
<width>450</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="showKnownBadCheckBox">
<property name="text">
<string>Also show entries that have been excluded from reports</string>
</property>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="noNetwork">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="networkNoticeLabel">
<property name="maximumSize">
<size>
<width>450</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View File

@ -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(),

View File

@ -6,11 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<width>397</width>
<height>379</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<property name="leftMargin">
<number>0</number>
</property>
@ -24,52 +24,43 @@
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="statisticsGroupBox">
<property name="title">
<string>Statistics</string>
<widget class="QTableView" name="statisticsTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="tipLabel">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Hover over lines with error icons for further information.</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="statisticsTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="tipLabel">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Hover over lines with error icons for further information.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>

View File

@ -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<QCheckBox*>("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");

View File

@ -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 ;;