/* * Copyright (C) 2019 KeePassXC Team * * 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 . */ #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 #include #include 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 db) : modified(QFileInfo(db->filePath()).lastModified()) , m_db(db) { gatherStats(db->rootGroup()->groupsRecursive(true)); } // Get average password length int averagePwdLength() const { return m_passwords.empty() ? 0 : pwdTotalLen / m_passwords.size(); } // 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 m_db; QHash m_passwords; void gatherStats(const QList& 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(); 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 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(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 }