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/group-new.svg
share/icons/application/scalable/actions/help-about.svg share/icons/application/scalable/actions/help-about.svg
share/icons/application/scalable/actions/key-enter.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/message-close.svg
share/icons/application/scalable/actions/move-down.svg share/icons/application/scalable/actions/move-down.svg
share/icons/application/scalable/actions/move-up.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/help-about.svg</file>
<file>application/scalable/actions/hibp.svg</file> <file>application/scalable/actions/hibp.svg</file>
<file>application/scalable/actions/key-enter.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/keyboard-shortcuts.svg</file>
<file>application/scalable/actions/message-close.svg</file> <file>application/scalable/actions/message-close.svg</file>
<file>application/scalable/actions/move-down.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(); password = in.readLine();
} }
estimate(password.toLatin1(), parser->isSet(Estimate::AdvancedOption)); estimate(password.toUtf8(), parser->isSet(Estimate::AdvancedOption));
return EXIT_SUCCESS; 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::Created = QStringLiteral("_CREATED");
const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_"); const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: "); const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
const QString CustomData::ExcludeFromReports = QStringLiteral("KnownBad");
CustomData::CustomData(QObject* parent) CustomData::CustomData(QObject* parent)
: QObject(parent) : QObject(parent)

View File

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

View File

@ -17,14 +17,12 @@
*/ */
#include "Entry.h" #include "Entry.h"
#include "config-keepassx.h"
#include "core/Clock.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Database.h" #include "core/Database.h"
#include "core/DatabaseIcons.h" #include "core/DatabaseIcons.h"
#include "core/Group.h" #include "core/Group.h"
#include "core/Metadata.h" #include "core/Metadata.h"
#include "core/PasswordHealth.h"
#include "core/Tools.h" #include "core/Tools.h"
#include "totp/totp.h" #include "totp/totp.h"
@ -245,6 +243,25 @@ QString Entry::defaultAutoTypeSequence() const
return m_data.defaultAutoTypeSequence; 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 * 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 * 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) void Entry::setPassword(const QString& password)
{ {
// Reset Password Health
m_data.passwordHealth.reset();
m_attributes->set(EntryAttributes::PasswordKey, password, m_attributes->isProtected(EntryAttributes::PasswordKey)); m_attributes->set(EntryAttributes::PasswordKey, password, m_attributes->isProtected(EntryAttributes::PasswordKey));
} }

View File

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

View File

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

View File

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

View File

@ -440,7 +440,7 @@ void EditEntryWidget::setupEntryUpdate()
// Advanced tab // Advanced tab
connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified())); connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), 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->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->bgColorCheckBox, 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())); connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
@ -861,9 +861,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
editTriggers = QAbstractItemView::DoubleClicked; editTriggers = QAbstractItemView::DoubleClicked;
} }
m_advancedUi->attributesView->setEditTriggers(editTriggers); m_advancedUi->attributesView->setEditTriggers(editTriggers);
m_advancedUi->knownBadCheckBox->setChecked(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) m_advancedUi->excludeReportsCheckBox->setChecked(entry->excludeFromReports());
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD)
== TRUE_STR);
setupColorButton(true, entry->foregroundColor()); setupColorButton(true, entry->foregroundColor());
setupColorButton(false, entry->backgroundColor()); setupColorButton(false, entry->backgroundColor());
m_iconsWidget->setEnabled(!m_history); m_iconsWidget->setEnabled(!m_history);
@ -1126,11 +1124,8 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
entry->setNotes(m_mainUi->notesEdit->toPlainText()); entry->setNotes(m_mainUi->notesEdit->toPlainText());
const auto wasKnownBad = entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) if (entry->excludeFromReports() != m_advancedUi->excludeReportsCheckBox->isChecked()) {
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR; entry->setExcludeFromReports(m_advancedUi->excludeReportsCheckBox->isChecked());
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()) { if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) {

View File

@ -187,9 +187,9 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="knownBadCheckBox"> <widget class="QCheckBox" name="excludeReportsCheckBox">
<property name="toolTip"> <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>
<property name="text"> <property name="text">
<string>Exclude from database reports</string> <string>Exclude from database reports</string>
@ -327,7 +327,7 @@
<tabstop>editAttributeButton</tabstop> <tabstop>editAttributeButton</tabstop>
<tabstop>protectAttributeButton</tabstop> <tabstop>protectAttributeButton</tabstop>
<tabstop>revealAttributeButton</tabstop> <tabstop>revealAttributeButton</tabstop>
<tabstop>knownBadCheckBox</tabstop> <tabstop>excludeReportsCheckBox</tabstop>
<tabstop>fgColorCheckBox</tabstop> <tabstop>fgColorCheckBox</tabstop>
<tabstop>fgColorButton</tabstop> <tabstop>fgColorButton</tabstop>
<tabstop>bgColorCheckBox</tabstop> <tabstop>bgColorCheckBox</tabstop>

View File

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

View File

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

View File

@ -20,12 +20,42 @@
#include <QAccessible> #include <QAccessible>
#include <QHeaderView> #include <QHeaderView>
#include <QKeyEvent>
#include <QMenu> #include <QMenu>
#include <QPainter>
#include <QShortcut> #include <QShortcut>
#include <QStyledItemDelegate>
#include "gui/SortFilterHideProxyModel.h" #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) EntryView::EntryView(QWidget* parent)
: QTreeView(parent) : QTreeView(parent)
, m_model(new EntryModel(this)) , m_model(new EntryModel(this))
@ -41,6 +71,7 @@ EntryView::EntryView(QWidget* parent)
// Use Qt::UserRole as sort role, see EntryModel::data() // Use Qt::UserRole as sort role, see EntryModel::data()
m_sortModel->setSortRole(Qt::UserRole); m_sortModel->setSortRole(Qt::UserRole);
QTreeView::setModel(m_sortModel); QTreeView::setModel(m_sortModel);
QTreeView::setItemDelegateForColumn(EntryModel::PasswordStrength, new PasswordStrengthItemDelegate(this));
setUniformRowHeights(true); setUniformRowHeights(true);
setRootIsDecorated(false); setRootIsDecorated(false);
@ -52,10 +83,10 @@ EntryView::EntryView(QWidget* parent)
// QAbstractItemView::startDrag() uses this property as the default drag action // QAbstractItemView::startDrag() uses this property as the default drag action
setDefaultDropAction(Qt::MoveAction); setDefaultDropAction(Qt::MoveAction);
// clang-format off
connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SLOT(emitEntrySelectionChanged())); connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] {
// clang-format on emit entrySelectionChanged(currentEntry());
});
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut); 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) { for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) {
int logicalIndex = header()->logicalIndex(visualIndex); int logicalIndex = header()->logicalIndex(visualIndex);
QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
if (logicalIndex == EntryModel::Paperclip) { if (caption.isEmpty()) {
caption = tr("Has attachments", "Entry attachment icon toggle"); caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole).toString();
} else if (logicalIndex == EntryModel::Totp) {
caption = tr("Has TOTP", "Entry TOTP icon toggle");
} }
QAction* action = m_headerMenu->addAction(caption); auto action = m_headerMenu->addAction(caption);
action->setCheckable(true); action->setCheckable(true);
action->setData(logicalIndex); action->setData(logicalIndex);
m_columnActions->addAction(action); m_columnActions->addAction(action);
@ -82,7 +111,8 @@ EntryView::EntryView(QWidget* parent)
connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*))); connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*)));
connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) { connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) {
Q_UNUSED(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(); m_headerMenu->addSeparator();
@ -101,8 +131,6 @@ EntryView::EntryView(QWidget* parent)
connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged())); connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged()));
connect(header(), SIGNAL(sectionResized(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))); connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), SLOT(sortIndicatorChanged(int, Qt::SortOrder)));
// clang-format off
} }
void EntryView::contextMenuShortcutPressed() 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 // do not emit any signals, header()->setSortIndicator recursively calls this
// function and the signals are emitted in the else part // function and the signals are emitted in the else part
} else { } 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 // this triggers the evaluation of the menu activation and anyway, the position
// of the selected entry within the widget did change // of the selected entry within the widget did change
emitEntrySelectionChanged(); emit entrySelectionChanged(currentEntry());
emit viewStateChanged(); emit viewStateChanged();
} }
} }
@ -228,11 +256,6 @@ void EntryView::emitEntryActivated(const QModelIndex& index)
emit entryActivated(entry, static_cast<EntryModel::ModelColumn>(m_sortModel->mapToSource(index).column())); emit entryActivated(entry, static_cast<EntryModel::ModelColumn>(m_sortModel->mapToSource(index).column()));
} }
void EntryView::emitEntrySelectionChanged()
{
emit entrySelectionChanged(currentEntry());
}
void EntryView::setModel(QAbstractItemModel* model) void EntryView::setModel(QAbstractItemModel* model)
{ {
Q_UNUSED(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() void EntryView::resetFixedColumns()
{ {
if (!isColumnHidden(EntryModel::Paperclip)) { for (const auto& col : {EntryModel::Paperclip, EntryModel::Totp, EntryModel::PasswordStrength}) {
header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed); if (!isColumnHidden(col)) {
header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize()); header()->setSectionResizeMode(col, QHeaderView::Fixed);
} header()->resizeSection(col, ICON_ONLY_SECTION_SIZE);
}
if (!isColumnHidden(EntryModel::Totp)) {
header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed);
header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize());
} }
} }
@ -431,6 +451,7 @@ void EntryView::resetViewToDefaults()
header()->hideSection(EntryModel::Accessed); header()->hideSection(EntryModel::Accessed);
header()->hideSection(EntryModel::Attachments); header()->hideSection(EntryModel::Attachments);
header()->hideSection(EntryModel::Size); header()->hideSection(EntryModel::Size);
header()->hideSection(EntryModel::PasswordStrength);
// Reset column order to logical indices // Reset column order to logical indices
for (int i = 0; i < header()->count(); ++i) { for (int i = 0; i < header()->count(); ++i) {

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,6 @@ public slots:
void makeHibpTable(); void makeHibpTable();
void customMenuRequested(QPoint); void customMenuRequested(QPoint);
void editFromContextmenu(); void editFromContextmenu();
void toggleKnownBad(bool);
private: private:
void startValidation(); void startValidation();
@ -78,7 +77,7 @@ private:
QList<const Entry*> m_rowToEntry; // List index is table row QList<const Entry*> m_rowToEntry; // List index is table row
QPointer<const Entry> m_editedEntry; // The entry we're currently editing QPointer<const Entry> m_editedEntry; // The entry we're currently editing
QString m_editedPassword; // The old password of the entry we're 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 Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING

View File

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

View File

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

View File

@ -461,14 +461,13 @@ void TestGui::testEditEntry()
// Test the "known bad" checkbox // Test the "known bad" checkbox
editEntryWidget->setCurrentPage(1); editEntryWidget->setCurrentPage(1);
auto knownBadCheckBox = editEntryWidget->findChild<QCheckBox*>("knownBadCheckBox"); auto excludeReportsCheckBox = editEntryWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
QVERIFY(knownBadCheckBox); QVERIFY(excludeReportsCheckBox);
QCOMPARE(knownBadCheckBox->isChecked(), false); QCOMPARE(excludeReportsCheckBox->isChecked(), false);
knownBadCheckBox->setChecked(true); excludeReportsCheckBox->setChecked(true);
QTest::mouseClick(applyButton, Qt::LeftButton); QTest::mouseClick(applyButton, Qt::LeftButton);
QCOMPARE(entry->historyItems().size(), ++editCount); QCOMPARE(entry->historyItems().size(), ++editCount);
QCOMPARE(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD), true); QVERIFY(entry->excludeFromReports());
QCOMPARE(entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD), TRUE_STR);
// Test entry colors (simulate choosing a color) // Test entry colors (simulate choosing a color)
editEntryWidget->setCurrentPage(1); editEntryWidget->setCurrentPage(1);