keepassxc/src/gui/reports/ReportsWidgetStatistics.cpp
Wolfram Rösler 66f5a8736a Statistics: Fix average password length
The average password length shown in the Statistics report
is now computed based on the total number of passwords.
Previously, it was erroneously computed based on the
number of unique passwords.

Fixes #5134.
2020-12-25 15:32:50 -05:00

260 lines
9.4 KiB
C++

/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ReportsWidgetStatistics.h"
#include "ui_ReportsWidgetStatistics.h"
#include "core/AsyncTask.h"
#include "core/Database.h"
#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/PasswordHealth.h"
#include "core/Resources.h"
#include <QFileInfo>
#include <QHash>
#include <QStandardItemModel>
namespace
{
class Stats
{
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
// Ctor does all the work
explicit Stats(QSharedPointer<Database> db)
: modified(QFileInfo(db->filePath()).lastModified())
, m_db(db)
{
gatherStats(db->rootGroup()->groupsRecursive(true));
}
// Get average password length
int averagePwdLength() const
{
const auto nPwds = nPwdsUnique + nPwdsReused;
return nPwds == 0 ? 0 : std::round(pwdTotalLen / double(nPwds));
}
// Get max number of password reuse (=how many entries
// share the same password)
int maxPwdReuse() const
{
int ret = 0;
for (const auto& count : m_passwords) {
ret = std::max(ret, count);
}
return ret;
}
// A warning sign is displayed if one of the
// following returns true.
bool isAnyExpired() const
{
return nExpired > 0;
}
bool areTooManyPwdsReused() const
{
return nPwdsReused > nPwdsUnique / 10;
}
bool arePwdsReusedTooOften() const
{
return maxPwdReuse() > 3;
}
bool isAvgPwdTooShort() const
{
return averagePwdLength() < 10;
}
private:
QSharedPointer<Database> m_db;
QHash<QString, int> m_passwords;
void gatherStats(const QList<Group*>& groups)
{
auto checker = HealthChecker(m_db);
for (const auto* group : groups) {
// Don't count anything in the recycle bin
if (group->isRecycled()) {
continue;
}
++nGroups;
for (const auto* entry : group->entries()) {
// Don't count anything in the recycle bin
if (entry->isRecycled()) {
continue;
}
++nEntries;
if (entry->isExpired()) {
++nExpired;
}
// Get password statistics
const auto pwd = entry->password();
if (!pwd.isEmpty()) {
if (!m_passwords.contains(pwd)) {
++nPwdsUnique;
} else {
++nPwdsReused;
}
if (pwd.size() < 8) {
++nPwdsShort;
}
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
++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]++;
}
}
}
}
};
} // namespace
ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ReportsWidgetStatistics())
, m_errIcon(Resources::instance()->icon("dialog-error"))
{
m_ui->setupUi(this);
m_referencesModel.reset(new QStandardItemModel());
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Value"));
m_ui->statisticsTableView->setModel(m_referencesModel.data());
m_ui->statisticsTableView->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->statisticsTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
}
ReportsWidgetStatistics::~ReportsWidgetStatistics()
{
}
void ReportsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg)
{
auto row = QList<QStandardItem*>();
row << new QStandardItem(name);
row << new QStandardItem(value);
m_referencesModel->appendRow(row);
if (bad) {
m_referencesModel->item(m_referencesModel->rowCount() - 1, 1)->setIcon(m_errIcon);
if (!badMsg.isEmpty()) {
m_referencesModel->item(m_referencesModel->rowCount() - 1, 1)->setToolTip(badMsg);
}
}
};
void ReportsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
{
m_db = std::move(db);
m_statsCalculated = false;
m_referencesModel->clear();
addStatsRow(tr("Please wait, database statistics are being calculated..."), "");
}
void ReportsWidgetStatistics::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
if (!m_statsCalculated) {
// Perform stats calculation on next event loop to allow widget to appear
m_statsCalculated = true;
QTimer::singleShot(0, this, SLOT(calculateStats()));
}
}
void ReportsWidgetStatistics::calculateStats()
{
const QScopedPointer<Stats> stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }));
m_referencesModel->clear();
addStatsRow(tr("Database name"), m_db->metadata()->name());
addStatsRow(tr("Description"), m_db->metadata()->description());
addStatsRow(tr("Location"), m_db->filePath());
addStatsRow(tr("Last saved"), stats->modified.toString(Qt::DefaultLocaleShortDate));
addStatsRow(tr("Unsaved changes"),
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 expired entries"),
QString::number(stats->nExpired),
stats->isAnyExpired(),
tr("The database contains entries that have expired."));
addStatsRow(tr("Unique passwords"), QString::number(stats->nPwdsUnique));
addStatsRow(tr("Non-unique passwords"),
QString::number(stats->nPwdsReused),
stats->areTooManyPwdsReused(),
tr("More than 10% of passwords are reused. Use unique passwords when possible."));
addStatsRow(tr("Maximum password reuse"),
QString::number(stats->maxPwdReuse()),
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,
tr("Recommended minimum password length is at least 8 characters."));
addStatsRow(tr("Number of weak passwords"),
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(),
tr("Average password length is less than ten characters. Longer passwords provide more security."));
}
void ReportsWidgetStatistics::saveSettings()
{
// nothing to do - the tab is passive
}