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

@ -51,6 +51,8 @@
<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/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>
@ -23,14 +23,11 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="healthcheckGroupBox">
<property name="title">
<string>Health Check</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="healthcheckTableView">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
@ -47,7 +44,7 @@
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
@ -57,6 +54,13 @@
</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">
@ -71,9 +75,6 @@
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

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,16 +11,22 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Have I Been Pwned?</string>
<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>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
<number>1</number>
</property>
<widget class="QWidget" name="confirmation">
<layout class="QVBoxLayout" name="verticalLayout_2">
@ -39,6 +45,19 @@
<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>
@ -82,13 +101,6 @@
</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">
@ -102,6 +114,13 @@
</property>
</spacer>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="resultsTable">
@ -120,6 +139,9 @@
</property>
<item>
<widget class="QTableView" name="hibpTableView">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
@ -135,6 +157,9 @@
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
@ -143,10 +168,30 @@
</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>
</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">
@ -182,9 +227,6 @@
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

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>
@ -23,12 +23,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="statisticsGroupBox">
<property name="title">
<string>Statistics</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="statisticsTableView">
<property name="editTriggers">
@ -71,9 +65,6 @@
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

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