Add entry view column for password strength

* Closes #4216

Reduced to three-tiered rating system and fixed column implementation. Hide password strength indicator in entry view if excluded from reports.

Introduce password health caching to prevent unnecessary calculations.
This commit is contained in:
Ojas Anand 2021-02-26 16:43:23 -05:00 committed by Jonathan White
parent c9c19d043f
commit 022154462e
23 changed files with 213 additions and 187 deletions

View File

@ -166,6 +166,7 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg
share/icons/application/scalable/actions/group-new.svg
share/icons/application/scalable/actions/help-about.svg
share/icons/application/scalable/actions/key-enter.svg
share/icons/application/scalable/actions/lock-question.svg
share/icons/application/scalable/actions/message-close.svg
share/icons/application/scalable/actions/move-down.svg
share/icons/application/scalable/actions/move-up.svg

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" width="24" height="24" viewBox="0 0 24 24"><path d="M12,1A5,5 0 0,0 7,6V8H6A2,2 0 0,0 4,10V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V10A2,2 0 0,0 18,8H17V6A5,5 0 0,0 12,1M12,2.9C13.71,2.9 15.1,4.29 15.1,6V8H8.9V6C8.9,4.29 10.29,2.9 12,2.9M12.19,10.5C13.13,10.5 13.88,10.71 14.42,11.12C14.96,11.54 15.23,12.1 15.23,12.8C15.23,13.24 15.08,13.63 14.79,14C14.5,14.36 14.12,14.64 13.66,14.85C13.4,15 13.23,15.15 13.14,15.32C13.05,15.5 13,15.72 13,16H11C11,15.5 11.1,15.16 11.29,14.92C11.5,14.68 11.84,14.4 12.36,14.08C12.62,13.94 12.83,13.76 13,13.54C13.14,13.33 13.22,13.08 13.22,12.8C13.22,12.5 13.13,12.28 12.95,12.11C12.77,11.93 12.5,11.85 12.19,11.85C11.92,11.85 11.7,11.92 11.5,12.06C11.34,12.2 11.24,12.41 11.24,12.69H9.27C9.22,12 9.5,11.4 10.05,11.04C10.59,10.68 11.3,10.5 12.19,10.5M11,17H13V19H11V17Z" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -46,6 +46,7 @@
<file>application/scalable/actions/help-about.svg</file>
<file>application/scalable/actions/hibp.svg</file>
<file>application/scalable/actions/key-enter.svg</file>
<file>application/scalable/actions/lock-question.svg</file>
<file>application/scalable/actions/keyboard-shortcuts.svg</file>
<file>application/scalable/actions/message-close.svg</file>
<file>application/scalable/actions/move-down.svg</file>

View File

@ -172,6 +172,6 @@ int Estimate::execute(const QStringList& arguments)
password = in.readLine();
}
estimate(password.toLatin1(), parser->isSet(Estimate::AdvancedOption));
estimate(password.toUtf8(), parser->isSet(Estimate::AdvancedOption));
return EXIT_SUCCESS;
}

View File

@ -24,6 +24,7 @@ const QString CustomData::LastModified = QStringLiteral("_LAST_MODIFIED");
const QString CustomData::Created = QStringLiteral("_CREATED");
const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
const QString CustomData::ExcludeFromReports = QStringLiteral("KnownBad");
CustomData::CustomData(QObject* parent)
: QObject(parent)

View File

@ -51,6 +51,7 @@ public:
static const QString Created;
static const QString BrowserKeyPrefix;
static const QString BrowserLegacyKeyPrefix;
static const QString ExcludeFromReports;
signals:
void customDataModified();

View File

@ -17,14 +17,12 @@
*/
#include "Entry.h"
#include "config-keepassx.h"
#include "core/Clock.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/PasswordHealth.h"
#include "core/Tools.h"
#include "totp/totp.h"
@ -245,6 +243,25 @@ QString Entry::defaultAutoTypeSequence() const
return m_data.defaultAutoTypeSequence;
}
const QSharedPointer<PasswordHealth>& Entry::passwordHealth()
{
if (!m_data.passwordHealth) {
m_data.passwordHealth.reset(new PasswordHealth(resolvePlaceholder(password())));
}
return m_data.passwordHealth;
}
bool Entry::excludeFromReports() const
{
return customData()->contains(CustomData::ExcludeFromReports)
&& customData()->value(CustomData::ExcludeFromReports) == TRUE_STR;
}
void Entry::setExcludeFromReports(bool state)
{
customData()->set(CustomData::ExcludeFromReports, state ? TRUE_STR : FALSE_STR);
}
/**
* Determine the effective sequence that will be injected
* This function return an empty string if a parent group has autotype disabled or if the entry has no parent
@ -673,6 +690,8 @@ void Entry::setUsername(const QString& username)
void Entry::setPassword(const QString& password)
{
// Reset Password Health
m_data.passwordHealth.reset();
m_attributes->set(EntryAttributes::PasswordKey, password, m_attributes->isProtected(EntryAttributes::PasswordKey));
}

View File

@ -36,6 +36,8 @@
class Database;
class Group;
class PasswordHealth;
namespace Totp
{
struct Settings;
@ -66,6 +68,7 @@ struct EntryData
QString defaultAutoTypeSequence;
TimeInfo timeInfo;
QSharedPointer<Totp::Settings> totpSettings;
QSharedPointer<PasswordHealth> passwordHealth;
bool operator==(const EntryData& other) const;
bool operator!=(const EntryData& other) const;
@ -94,7 +97,6 @@ public:
int autoTypeObfuscation() const;
QString defaultAutoTypeSequence() const;
QString effectiveAutoTypeSequence() const;
QString effectiveNewAutoTypeSequence() const;
QList<QString> autoTypeSequences(const QString& pattern = {}) const;
AutoTypeAssociations* autoTypeAssociations();
const AutoTypeAssociations* autoTypeAssociations() const;
@ -111,6 +113,9 @@ public:
QSharedPointer<Totp::Settings> totpSettings() const;
int size() const;
QString path() const;
const QSharedPointer<PasswordHealth>& passwordHealth();
bool excludeFromReports() const;
void setExcludeFromReports(bool state);
bool hasTotp() const;
bool isExpired() const;

View File

@ -24,9 +24,6 @@
#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)
@ -49,8 +46,8 @@ PasswordHealth::PasswordHealth(double entropy)
}
}
PasswordHealth::PasswordHealth(QString pwd)
: PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
PasswordHealth::PasswordHealth(const QString& pwd)
: PasswordHealth(ZxcvbnMatch(pwd.toUtf8(), nullptr, nullptr))
{
}

View File

@ -34,7 +34,7 @@ class PasswordHealth
{
public:
explicit PasswordHealth(double entropy);
explicit PasswordHealth(QString pwd);
explicit PasswordHealth(const QString& pwd);
/*
* The password score is defined to be the greater the better
@ -83,14 +83,6 @@ 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

@ -440,7 +440,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->excludeReportsCheckBox, 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()));
@ -861,9 +861,7 @@ 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);
m_advancedUi->excludeReportsCheckBox->setChecked(entry->excludeFromReports());
setupColorButton(true, entry->foregroundColor());
setupColorButton(false, entry->backgroundColor());
m_iconsWidget->setEnabled(!m_history);
@ -1126,11 +1124,8 @@ 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 (entry->excludeFromReports() != m_advancedUi->excludeReportsCheckBox->isChecked()) {
entry->setExcludeFromReports(m_advancedUi->excludeReportsCheckBox->isChecked());
}
if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) {

View File

@ -187,9 +187,9 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="knownBadCheckBox">
<widget class="QCheckBox" name="excludeReportsCheckBox">
<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>
<string>If checked, the entry will not appear in reports like Health Check and HIBP even if it doesn't match the quality requirements.</string>
</property>
<property name="text">
<string>Exclude from database reports</string>
@ -327,7 +327,7 @@
<tabstop>editAttributeButton</tabstop>
<tabstop>protectAttributeButton</tabstop>
<tabstop>revealAttributeButton</tabstop>
<tabstop>knownBadCheckBox</tabstop>
<tabstop>excludeReportsCheckBox</tabstop>
<tabstop>fgColorCheckBox</tabstop>
<tabstop>fgColorButton</tabstop>
<tabstop>bgColorCheckBox</tabstop>

View File

@ -18,18 +18,17 @@
#include "EntryModel.h"
#include <QDateTime>
#include <QFont>
#include <QMimeData>
#include <QPainter>
#include <QPalette>
#include "core/Config.h"
#include "core/DatabaseIcons.h"
#include "core/Entry.h"
#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/PasswordHealth.h"
#include "gui/Icons.h"
#include "gui/styles/StateColorPalette.h"
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
#endif
@ -128,7 +127,7 @@ int EntryModel::columnCount(const QModelIndex& parent) const
return 0;
}
return 14;
return 15;
}
QVariant EntryModel::data(const QModelIndex& index, int role) const
@ -249,6 +248,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
return entry->resolveMultiplePlaceholders(entry->username());
case Password:
return entry->resolveMultiplePlaceholders(entry->password());
case PasswordStrength: {
if (!entry->password().isEmpty() && !entry->excludeFromReports()) {
return entry->passwordHealth()->score();
}
return 0;
}
case Expires:
// There seems to be no better way of expressing 'infinity'
return entry->timeInfo().expires() ? entry->timeInfo().expiryTime() : QDateTime(QDate(9999, 1, 1));
@ -290,6 +295,28 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
return icons()->icon("chronometer");
}
break;
case PasswordStrength:
if (!entry->password().isEmpty() && !entry->excludeFromReports()) {
StateColorPalette statePalette;
QColor color = statePalette.color(StateColorPalette::Error);
switch (entry->passwordHealth()->quality()) {
case PasswordHealth::Quality::Bad:
case PasswordHealth::Quality::Poor:
color = statePalette.color(StateColorPalette::HealthCritical);
break;
case PasswordHealth::Quality::Weak:
color = statePalette.color(StateColorPalette::HealthBad);
break;
case PasswordHealth::Quality::Good:
case PasswordHealth::Quality::Excellent:
color = statePalette.color(StateColorPalette::HealthExcellent);
break;
}
return color;
}
break;
}
} else if (role == Qt::FontRole) {
QFont font;
@ -316,9 +343,9 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
if (backgroundColor.isValid()) {
return QVariant(backgroundColor);
}
} else if (role == Qt::TextAlignmentRole) {
if (index.column() == Paperclip) {
return Qt::AlignCenter;
} else if (role == Qt::ToolTipRole) {
if (index.column() == PasswordStrength && !entry->password().isEmpty() && !entry->excludeFromReports()) {
return entry->passwordHealth()->scoreReason();
}
}
@ -363,6 +390,8 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
return icons()->icon("paperclip");
case Totp:
return icons()->icon("chronometer");
case PasswordStrength:
return icons()->icon("lock-question");
}
} else if (role == Qt::ToolTipRole) {
switch (section) {
@ -374,6 +403,8 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
return tr("Username");
case Password:
return tr("Password");
case PasswordStrength:
return tr("Password Strength");
case Url:
return tr("URL");
case Notes:
@ -393,7 +424,7 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
case Paperclip:
return tr("Has attachments");
case Totp:
return tr("Has TOTP one-time password");
return tr("Has TOTP");
}
}

View File

@ -46,7 +46,8 @@ public:
Paperclip = 10,
Attachments = 11,
Totp = 12,
Size = 13
Size = 13,
PasswordStrength = 14
};
explicit EntryModel(QObject* parent = nullptr);

View File

@ -20,12 +20,42 @@
#include <QAccessible>
#include <QHeaderView>
#include <QKeyEvent>
#include <QMenu>
#include <QPainter>
#include <QShortcut>
#include <QStyledItemDelegate>
#include "gui/SortFilterHideProxyModel.h"
#define ICON_ONLY_SECTION_SIZE 26
class PasswordStrengthItemDelegate : public QStyledItemDelegate
{
public:
explicit PasswordStrengthItemDelegate(QObject* parent)
: QStyledItemDelegate(parent){};
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
{
QStyledItemDelegate::initStyleOption(option, index);
auto value = index.data(Qt::DecorationRole);
if (value.isValid() && value.type() == QVariant::Color && option->rect.width() > 0) {
// Rebuild the password strength icon to add a dark border
QColor pen(Qt::black);
if (option->widget) {
pen = option->widget->palette().color(QPalette::Shadow);
}
auto size = option->decorationSize;
QImage image(size.width(), size.height(), QImage::Format_ARGB32_Premultiplied);
QPainter p(&image);
p.setBrush(value.value<QColor>());
p.setPen(pen);
p.drawRect(0, 0, size.width() - 1, size.height() - 1);
option->icon = QIcon(QPixmap::fromImage(image));
}
}
};
EntryView::EntryView(QWidget* parent)
: QTreeView(parent)
, m_model(new EntryModel(this))
@ -41,6 +71,7 @@ EntryView::EntryView(QWidget* parent)
// Use Qt::UserRole as sort role, see EntryModel::data()
m_sortModel->setSortRole(Qt::UserRole);
QTreeView::setModel(m_sortModel);
QTreeView::setItemDelegateForColumn(EntryModel::PasswordStrength, new PasswordStrengthItemDelegate(this));
setUniformRowHeights(true);
setRootIsDecorated(false);
@ -52,10 +83,10 @@ EntryView::EntryView(QWidget* parent)
// QAbstractItemView::startDrag() uses this property as the default drag action
setDefaultDropAction(Qt::MoveAction);
// clang-format off
connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SLOT(emitEntrySelectionChanged()));
// clang-format on
connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] {
emit entrySelectionChanged(currentEntry());
});
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut);
@ -68,13 +99,11 @@ EntryView::EntryView(QWidget* parent)
for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) {
int logicalIndex = header()->logicalIndex(visualIndex);
QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
if (logicalIndex == EntryModel::Paperclip) {
caption = tr("Has attachments", "Entry attachment icon toggle");
} else if (logicalIndex == EntryModel::Totp) {
caption = tr("Has TOTP", "Entry TOTP icon toggle");
if (caption.isEmpty()) {
caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole).toString();
}
QAction* action = m_headerMenu->addAction(caption);
auto action = m_headerMenu->addAction(caption);
action->setCheckable(true);
action->setData(logicalIndex);
m_columnActions->addAction(action);
@ -82,7 +111,8 @@ EntryView::EntryView(QWidget* parent)
connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*)));
connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) {
Q_UNUSED(order)
header()->setSortIndicatorShown(index != EntryModel::Paperclip && index != EntryModel::Totp);
header()->setSortIndicatorShown(index != EntryModel::Paperclip && index != EntryModel::Totp
&& index != EntryModel::PasswordStrength);
});
m_headerMenu->addSeparator();
@ -101,8 +131,6 @@ EntryView::EntryView(QWidget* parent)
connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged()));
connect(header(), SIGNAL(sectionResized(int, int, int)), SIGNAL(viewStateChanged()));
connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), SLOT(sortIndicatorChanged(int, Qt::SortOrder)));
// clang-format off
}
void EntryView::contextMenuShortcutPressed()
@ -130,10 +158,10 @@ void EntryView::sortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
// do not emit any signals, header()->setSortIndicator recursively calls this
// function and the signals are emitted in the else part
} else {
// call emitEntrySelectionChanged even though the selection did not really change
// emit entrySelectionChanged even though the selection did not really change
// this triggers the evaluation of the menu activation and anyway, the position
// of the selected entry within the widget did change
emitEntrySelectionChanged();
emit entrySelectionChanged(currentEntry());
emit viewStateChanged();
}
}
@ -228,11 +256,6 @@ void EntryView::emitEntryActivated(const QModelIndex& index)
emit entryActivated(entry, static_cast<EntryModel::ModelColumn>(m_sortModel->mapToSource(index).column()));
}
void EntryView::emitEntrySelectionChanged()
{
emit entrySelectionChanged(currentEntry());
}
void EntryView::setModel(QAbstractItemModel* model)
{
Q_UNUSED(model);
@ -391,18 +414,15 @@ void EntryView::fitColumnsToContents()
}
/**
* Mark icon-only columns as fixed and resize them to their minimum section size.
* Mark icon-only columns as fixed and resize them to icon-only section size
*/
void EntryView::resetFixedColumns()
{
if (!isColumnHidden(EntryModel::Paperclip)) {
header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed);
header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize());
}
if (!isColumnHidden(EntryModel::Totp)) {
header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed);
header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize());
for (const auto& col : {EntryModel::Paperclip, EntryModel::Totp, EntryModel::PasswordStrength}) {
if (!isColumnHidden(col)) {
header()->setSectionResizeMode(col, QHeaderView::Fixed);
header()->resizeSection(col, ICON_ONLY_SECTION_SIZE);
}
}
}
@ -431,6 +451,7 @@ void EntryView::resetViewToDefaults()
header()->hideSection(EntryModel::Accessed);
header()->hideSection(EntryModel::Attachments);
header()->hideSection(EntryModel::Size);
header()->hideSection(EntryModel::PasswordStrength);
// Reset column order to logical indices
for (int i = 0; i < header()->count(); ++i) {

View File

@ -63,7 +63,6 @@ protected:
private slots:
void emitEntryActivated(const QModelIndex& index);
void emitEntrySelectionChanged();
void showHeaderMenu(const QPoint& position);
void toggleColumnVisibility(QAction* action);
void fitColumnsToWindow();

View File

@ -41,14 +41,13 @@ namespace
QPointer<const Group> group;
QPointer<const Entry> entry;
QSharedPointer<PasswordHealth> health;
bool knownBad = false;
bool exclude = 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)
, exclude(e->excludeFromReports())
{
}
@ -121,7 +120,7 @@ Health::Health(QSharedPointer<Database> db)
// Evaluate this entry
const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry)));
if (item->knownBad) {
if (item->exclude) {
m_anyKnownBad = true;
}
@ -140,7 +139,6 @@ Health::Health(QSharedPointer<Database> db)
ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ReportsWidgetHealthcheck())
, m_errorIcon(icons()->icon("dialog-error"))
, m_referencesModel(new QStandardItemModel(this))
, m_modelProxy(new ReportSortProxyModel(this))
{
@ -258,18 +256,18 @@ void ReportsWidgetHealthcheck::calculateHealth()
const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
// Display entries that are marked as "known bad"?
const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
const auto showExcluded = m_ui->showKnownBadCheckBox->isChecked();
// Display the entries
m_rowToEntry.clear();
for (const auto& item : health->items()) {
if (item->knownBad && !showKnownBad) {
if (item->exclude && !showExcluded) {
// Exclude this entry from the report
continue;
}
// Show the entry in the report
addHealthRow(item->health, item->group, item->entry, item->knownBad);
addHealthRow(item->health, item->group, item->entry, item->exclude);
}
// Set the table header
@ -330,12 +328,16 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
// Create the "exclude from reports" menu item
const auto knownbad = new QAction(icons()->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)));
const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
exclude->setCheckable(true);
exclude->setChecked(m_contextmenuEntry->excludeFromReports());
menu->addAction(exclude);
connect(exclude, &QAction::toggled, exclude, [this](bool state) {
if (m_contextmenuEntry) {
m_contextmenuEntry->setExcludeFromReports(state);
calculateHealth();
}
});
// Show the context menu
menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos));
@ -348,17 +350,6 @@ void ReportsWidgetHealthcheck::editFromContextmenu()
}
}
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

@ -41,7 +41,7 @@ class ReportsWidgetHealthcheck : public QWidget
Q_OBJECT
public:
explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr);
~ReportsWidgetHealthcheck();
~ReportsWidgetHealthcheck() override;
void loadSettings(QSharedPointer<Database> db);
void saveSettings();
@ -57,7 +57,6 @@ public slots:
void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
void editFromContextmenu();
void toggleKnownBad(bool);
private:
void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
@ -65,7 +64,6 @@ private:
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
bool m_healthCalculated = false;
QIcon m_errorIcon;
QScopedPointer<QStandardItemModel> m_referencesModel;
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
QSharedPointer<Database> m_db;

View File

@ -32,20 +32,6 @@
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;
}
class ReportSortProxyModel : public QSortFilterProxyModel
{
public:
@ -153,25 +139,23 @@ void ReportsWidgetHibp::makeHibpTable()
});
// Display entries that are marked as "known bad"?
const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
const auto showExcluded = m_ui->showKnownBadCheckBox->isChecked();
// The colors for table cells
const auto red = QBrush("red");
// Build the table
bool anyKnownBad = false;
bool anyExcluded = 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) {
// Hide entry if excluded unless explicitly requested
if (entry->excludeFromReports()) {
anyExcluded = true;
if (!showExcluded) {
continue;
}
@ -183,7 +167,7 @@ void ReportsWidgetHibp::makeHibpTable()
<< new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"))
<< new QStandardItem(countToText(count));
if (knownBad) {
if (entry->excludeFromReports()) {
row[1]->setToolTip(tr("This entry is being excluded from reports"));
}
@ -213,7 +197,7 @@ void ReportsWidgetHibp::makeHibpTable()
// Show the "show known bad entries" checkbox if there's any known
// bad entry in the database.
if (anyKnownBad) {
if (anyExcluded) {
m_ui->showKnownBadCheckBox->show();
} else {
m_ui->showKnownBadCheckBox->hide();
@ -331,7 +315,7 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index)
// Found it, invoke entry editor
m_editedEntry = entry;
m_editedPassword = entry->password();
m_editedKnownBad = isKnownBad(entry);
m_editedExcluded = entry->excludeFromReports();
emit entryActivated(const_cast<Entry*>(entry));
}
}
@ -350,7 +334,7 @@ void ReportsWidgetHibp::refreshAfterEdit()
// 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) {
if (m_editedEntry->password() == m_editedPassword && m_editedEntry->excludeFromReports() == m_editedExcluded) {
// Don't go through HIBP but still rebuild the table, the user might
// have edited the entry title.
makeHibpTable();
@ -392,12 +376,16 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos)
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
// Create the "exclude from reports" menu item
const auto knownbad = new QAction(icons()->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)));
const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
exclude->setCheckable(true);
exclude->setChecked(m_contextmenuEntry->excludeFromReports());
menu->addAction(exclude);
connect(exclude, &QAction::toggled, exclude, [this](bool state) {
if (m_contextmenuEntry) {
m_contextmenuEntry->setExcludeFromReports(state);
makeHibpTable();
}
});
// Show the context menu
menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
@ -410,17 +398,6 @@ void ReportsWidgetHibp::editFromContextmenu()
}
}
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

@ -62,7 +62,6 @@ public slots:
void makeHibpTable();
void customMenuRequested(QPoint);
void editFromContextmenu();
void toggleKnownBad(bool);
private:
void startValidation();
@ -78,7 +77,7 @@ 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
bool m_editedExcluded; // 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

View File

@ -26,8 +26,6 @@
#include "core/PasswordHealth.h"
#include "gui/Icons.h"
#include <QFileInfo>
#include <QHash>
#include <QStandardItemModel>
namespace
@ -37,15 +35,15 @@ namespace
public:
// The statistics we collect:
QDateTime modified; // File modification time
int nGroups = 0; // Number of groups in the database
int nEntries = 0; // Number of entries (across all groups)
int nExpired = 0; // Number of expired entries
int nPwdsWeak = 0; // Number of weak or poor passwords
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
int groupCount = 0; // Number of groups in the database
int entryCount = 0; // Number of entries (across all groups)
int expiredEntries = 0; // Number of expired entries
int excludedEntries = 0; // Number of known bad entries
int weakPasswords = 0; // Number of weak or poor passwords
int shortPasswords = 0; // Number of passwords 8 characters or less in size
int uniquePasswords = 0; // Number of unique passwords
int reusedPasswords = 0; // Number of non-unique passwords
int totalPasswordLength = 0; // Total length of all passwords
// Ctor does all the work
explicit Stats(QSharedPointer<Database> db)
@ -58,8 +56,8 @@ namespace
// Get average password length
int averagePwdLength() const
{
const auto nPwds = nPwdsUnique + nPwdsReused;
return nPwds == 0 ? 0 : std::round(pwdTotalLen / double(nPwds));
const auto passwords = uniquePasswords + reusedPasswords;
return passwords == 0 ? 0 : std::round(totalPasswordLength / double(passwords));
}
// Get max number of password reuse (=how many entries
@ -77,12 +75,12 @@ namespace
// following returns true.
bool isAnyExpired() const
{
return nExpired > 0;
return expiredEntries > 0;
}
bool areTooManyPwdsReused() const
{
return nPwdsReused > nPwdsUnique / 10;
return reusedPasswords > uniquePasswords / 10;
}
bool arePwdsReusedTooOften() const
@ -109,7 +107,7 @@ namespace
continue;
}
++nGroups;
++groupCount;
for (const auto* entry : group->entries()) {
// Don't count anything in the recycle bin
@ -117,36 +115,35 @@ namespace
continue;
}
++nEntries;
++entryCount;
if (entry->isExpired()) {
++nExpired;
++expiredEntries;
}
// Get password statistics
const auto pwd = entry->password();
if (!pwd.isEmpty()) {
if (!m_passwords.contains(pwd)) {
++nPwdsUnique;
++uniquePasswords;
} else {
++nPwdsReused;
++reusedPasswords;
}
if (pwd.size() < 8) {
++nPwdsShort;
++shortPasswords;
}
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
++nPwdsWeak;
++weakPasswords;
}
if (entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) {
++nKnownBad;
if (entry->excludeFromReports()) {
++excludedEntries;
}
pwdTotalLen += pwd.size();
totalPasswordLength += pwd.size();
m_passwords[pwd]++;
}
}
@ -220,15 +217,15 @@ void ReportsWidgetStatistics::calculateStats()
m_db->isModified() ? tr("yes") : tr("no"),
m_db->isModified(),
tr("The database was modified, but the changes have not yet been saved to disk."));
addStatsRow(tr("Number of groups"), QString::number(stats->nGroups));
addStatsRow(tr("Number of entries"), QString::number(stats->nEntries));
addStatsRow(tr("Number of groups"), QString::number(stats->groupCount));
addStatsRow(tr("Number of entries"), QString::number(stats->entryCount));
addStatsRow(tr("Number of expired entries"),
QString::number(stats->nExpired),
QString::number(stats->expiredEntries),
stats->isAnyExpired(),
tr("The database contains entries that have expired."));
addStatsRow(tr("Unique passwords"), QString::number(stats->nPwdsUnique));
addStatsRow(tr("Unique passwords"), QString::number(stats->uniquePasswords));
addStatsRow(tr("Non-unique passwords"),
QString::number(stats->nPwdsReused),
QString::number(stats->reusedPasswords),
stats->areTooManyPwdsReused(),
tr("More than 10% of passwords are reused. Use unique passwords when possible."));
addStatsRow(tr("Maximum password reuse"),
@ -236,16 +233,16 @@ void ReportsWidgetStatistics::calculateStats()
stats->arePwdsReusedTooOften(),
tr("Some passwords are used more than three times. Use unique passwords when possible."));
addStatsRow(tr("Number of short passwords"),
QString::number(stats->nPwdsShort),
stats->nPwdsShort > 0,
QString::number(stats->shortPasswords),
stats->shortPasswords > 0,
tr("Recommended minimum password length is at least 8 characters."));
addStatsRow(tr("Number of weak passwords"),
QString::number(stats->nPwdsWeak),
stats->nPwdsWeak > 0,
QString::number(stats->weakPasswords),
stats->weakPasswords > 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,
QString::number(stats->excludedEntries),
stats->excludedEntries > 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"),

View File

@ -321,7 +321,7 @@ void TestEntryModel::testProxyModel()
*/
QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int)));
modelProxy->hideColumn(0, true);
QCOMPARE(modelProxy->columnCount(), 13);
QCOMPARE(modelProxy->columnCount(), 14);
QVERIFY(!spyColumnRemove.isEmpty());
int oldSpyColumnRemoveSize = spyColumnRemove.size();
@ -343,7 +343,7 @@ void TestEntryModel::testProxyModel()
*/
QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int)));
modelProxy->hideColumn(0, false);
QCOMPARE(modelProxy->columnCount(), 14);
QCOMPARE(modelProxy->columnCount(), 15);
QVERIFY(!spyColumnInsert.isEmpty());
int oldSpyColumnInsertSize = spyColumnInsert.size();

View File

@ -461,14 +461,13 @@ void TestGui::testEditEntry()
// Test the "known bad" checkbox
editEntryWidget->setCurrentPage(1);
auto knownBadCheckBox = editEntryWidget->findChild<QCheckBox*>("knownBadCheckBox");
QVERIFY(knownBadCheckBox);
QCOMPARE(knownBadCheckBox->isChecked(), false);
knownBadCheckBox->setChecked(true);
auto excludeReportsCheckBox = editEntryWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
QVERIFY(excludeReportsCheckBox);
QCOMPARE(excludeReportsCheckBox->isChecked(), false);
excludeReportsCheckBox->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);
QVERIFY(entry->excludeFromReports());
// Test entry colors (simulate choosing a color)
editEntryWidget->setCurrentPage(1);