From 8afb1f17b49358b42c64f12c98d2b0d46dbf1954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= Date: Sun, 13 Oct 2019 11:45:58 -0400 Subject: [PATCH] Add "Statistics" page to Database Settings dialog (#2034) Added new page "Statistics" to the Database Settings dialog that shows information like number of groups and entries, number of unique and re-used passwords, average password length, etc. Show warnings for problematic values with explainations for the user in tooltips. Fixes #2034 Database statistics icon: Downloaded from: https://www.flaticon.com/authors/freepik Original source: https://www.flaticon.com/free-icon/bars-chart_265733 --- CHANGELOG.md | 1 + COPYING | 3 + .../application/32x32/actions/statistics.png | Bin 0 -> 1323 bytes src/CMakeLists.txt | 2 + src/core/Bootstrap.cpp | 2 +- src/gui/dbsettings/DatabaseSettingsDialog.cpp | 3 + .../DatabaseSettingsPageStatistics.cpp | 52 +++++ .../DatabaseSettingsPageStatistics.h | 35 +++ .../DatabaseSettingsWidgetBrowser.cpp | 12 +- .../DatabaseSettingsWidgetStatistics.cpp | 215 ++++++++++++++++++ .../DatabaseSettingsWidgetStatistics.h | 51 +++++ .../DatabaseSettingsWidgetStatistics.ui | 79 +++++++ 12 files changed, 448 insertions(+), 7 deletions(-) create mode 100644 share/icons/application/32x32/actions/statistics.png create mode 100644 src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp create mode 100644 src/gui/dbsettings/DatabaseSettingsPageStatistics.h create mode 100644 src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp create mode 100644 src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h create mode 100644 src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1e89ffdf..54222ca656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - CLI: Add group commands (mv, mkdir and rmdir) [#3313]. - CLI: Add interactive shell mode command `open` [#3224](https://github.com/keepassxreboot/keepassxc/issues/3224) - Add "Paper Backup" aka "Export to HTML file" to the "Database" menu [#3277](https://github.com/keepassxreboot/keepassxc/pull/3277) +- Add statistics panel with information about the database (number of entries, number of unique passwords, etc.) to the Database Settings dialog [#2034](https://github.com/keepassxreboot/keepassxc/issues/2034) ### Changed diff --git a/COPYING b/COPYING index a75f6c69d0..b7ba1abe4a 100644 --- a/COPYING +++ b/COPYING @@ -245,3 +245,6 @@ License: MIT Files: share/icons/application/scalable/apps/freedesktop.svg Copyright: GPL-2+ Comment: from Freedesktop.org website + +Files: share/icons/application/32x32/actions/statistics.png +Copyright: Icon made by Freepik from https://www.flaticon.com/free-icon/bars-chart_265733 diff --git a/share/icons/application/32x32/actions/statistics.png b/share/icons/application/32x32/actions/statistics.png new file mode 100644 index 0000000000000000000000000000000000000000..d5b67d0e5c2a1f413ccd4d5a1560f6e0f2c45d61 GIT binary patch literal 1323 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?gFHN;HUHMdLYGF z;1O92wCy?wGdgL^t^f+Mmw5WRvOnhJXJa=MPK|!Tz`*>~)5S3);_%*S{yie00>|pV z#`(WL$Fq5Z!feq*-lB^N68u7nfx=Upk5znGz^>}p%r1PfyVKBRZ-GKv_eG9l5{tua zD*3Qk*CzsqW2`{hxx^LC`c`{Zt zYHahE`52#Xx3f7IG>h4-;qC!uZ>gQ!4GjAt^mn>mJ$2!j+vZqPJh4bsiurd#Is@yuiP!IHUeEJ?&$Vi~%ZF|;yX6(40jya^ zt=Kv?EhtssQ97j`{N5qX`|#1LZ9gq+&q#Avy|-Q$B=_r#<@SSX+TH(eWBnmFgQd+tt_&|G4ue-2bK-IH@G&P6ic{%*XPpwvv1-hBpPm}eSWHTRDadg zQ~7G1yWFm_xgM0s_SvqG-6b|x&*5Y>V~5K6Q%B!0oQv75vby$>m6LLVi0%8lE3@TS z-HH_R_Ot}IkhOgInI=(NN+4*`z!ByygtSD^|O^C0ESUF-co=yKu!c;p=||9er$FcB^ZB`^Ns1 zk9mH<#(jIvSM{~EKVnwA%o};3vi;)yUJ?CA%dRdt&?G+b>wTNomLIR%Jt+LdYxmbG zJO1aa&)51FxHx`QHobk@n{%>{V1Zj)EklyU4hd#{M(OU`s~x6Grv2^6nxgV@y~AAA zDFG_agLnPo_p(?#G3w{u*`B-3UHNkHV(Fp4+pIPbDm7)F)xOEsKXjcfY+$nR;)&{Y z&8Mce6xAP4I=z{{NwQSy0N;a&kA7zAeV7`)^+B5G^{MqOZku-(%S^M^z?5?NB@;Da53hrf09W{!XOW>p7nK zO&(I3pYHPSua!G~@pGhD-;#st{^?D9tC^?j!0G*Nxus>**FzOGZ!c}*TlDhQi;drY z+&zBm&dufCMz!J%v$Er^RjXZIu`}qgcK_2CJkziKn{e&N#Mg)4E;rh^k*Ut=lGfjl zpz^4Fb8UXDlf3%KMnP3)@}dvCD$aYljQ7l6Dah*=viFhkyB~ zRwh8MiIsuD-@04}6b-rgDVb@NxHa@BU;P8rAPKS|I6tkVJh3R1p}f3YFEcN@I61K( YRWH9NefB#WDWD<-Pgg&ebxsLQ0OoZuDF6Tf literal 0 HcmV?d00001 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3565de443..acc94785d1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -148,6 +148,8 @@ set(keepassx_SOURCES gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp + gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp + gui/dbsettings/DatabaseSettingsPageStatistics.cpp gui/settings/SettingsWidget.cpp gui/widgets/ElidedLabel.cpp gui/widgets/PopupHelpWidget.cpp diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index 204942f4de..2d1a3e0878 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -257,7 +257,7 @@ namespace Bootstrap nullptr, // do not change owner or group pACL, // DACL specified nullptr // do not change SACL - ); + ); Cleanup: diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index e0e6765a46..33c4df2c4d 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -19,6 +19,7 @@ #include "DatabaseSettingsDialog.h" #include "ui_DatabaseSettingsDialog.h" +#include "DatabaseSettingsPageStatistics.h" #include "DatabaseSettingsWidgetEncryption.h" #include "DatabaseSettingsWidgetGeneral.h" #include "DatabaseSettingsWidgetMasterKey.h" @@ -84,6 +85,8 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key")); m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings")); + addSettingsPage(new DatabaseSettingsPageStatistics()); + #if defined(WITH_XC_KEESHARE) addSettingsPage(new DatabaseSettingsPageKeeShare()); #endif diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp b/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp new file mode 100644 index 0000000000..6fe24ff0f3 --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp @@ -0,0 +1,52 @@ +/* + * 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 "DatabaseSettingsPageStatistics.h" + +#include "DatabaseSettingsWidgetStatistics.h" +#include "core/Database.h" +#include "core/FilePath.h" +#include "core/Group.h" + +#include + +QString DatabaseSettingsPageStatistics::name() +{ + return QApplication::tr("Statistics"); +} + +QIcon DatabaseSettingsPageStatistics::icon() +{ + return FilePath::instance()->icon("actions", "statistics"); +} + +QWidget* DatabaseSettingsPageStatistics::createWidget() +{ + return new DatabaseSettingsWidgetStatistics(); +} + +void DatabaseSettingsPageStatistics::loadSettings(QWidget* widget, QSharedPointer db) +{ + DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast(widget); + settingsWidget->loadSettings(db); +} + +void DatabaseSettingsPageStatistics::saveSettings(QWidget* widget) +{ + DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast(widget); + settingsWidget->saveSettings(); +} diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h b/src/gui/dbsettings/DatabaseSettingsPageStatistics.h new file mode 100644 index 0000000000..c890f3b81c --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsPageStatistics.h @@ -0,0 +1,35 @@ +/* + * 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 . + */ + +#ifndef KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H +#define KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H + +#include + +#include "DatabaseSettingsDialog.h" + +class DatabaseSettingsPageStatistics : public IDatabaseSettingsPage +{ +public: + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget, QSharedPointer db) override; + void saveSettings(QWidget* widget) override; +}; + +#endif // KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp index 4ea30c1f6f..fac85c21e6 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp @@ -242,12 +242,12 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData() { if (MessageBox::Yes != MessageBox::question( - this, - tr("Move KeePassHTTP attributes to custom data"), - tr("Do you really want to move all legacy browser integration data to the latest standard?\n" - "This is necessary to maintain compatibility with the browser plugin."), - MessageBox::Yes | MessageBox::Cancel, - MessageBox::Cancel)) { + this, + tr("Move KeePassHTTP attributes to custom data"), + tr("Do you really want to move all legacy browser integration data to the latest standard?\n" + "This is necessary to maintain compatibility with the browser plugin."), + MessageBox::Yes | MessageBox::Cancel, + MessageBox::Cancel)) { return; } diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp new file mode 100644 index 0000000000..307f6c45e5 --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp @@ -0,0 +1,215 @@ +/* + * 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 "DatabaseSettingsWidgetStatistics.h" +#include "ui_DatabaseSettingsWidgetStatistics.h" + +#include "core/Database.h" +#include "core/FilePath.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "zxcvbn/zxcvbn.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 pwdTotalLen = 0; // Total length of all passwords + + // Ctor does all the work + explicit Stats(QSharedPointer db) + : modified(QFileInfo(db->filePath()).lastModified()) + { + 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: + QHash m_passwords; + + void gatherStats(const QList& groups) + { + for (const auto* group : groups) { + // Don't count anything in the recycle bin + if (group == group->database()->metadata()->recycleBin()) { + continue; + } + + ++nGroups; + + for (const auto* entry : group->entries()) { + ++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; + } + + if (ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr) < 65) { + ++nPwdsWeak; + } + + pwdTotalLen += pwd.size(); + m_passwords[pwd]++; + } + } + } + } + }; +} // namespace + +DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::DatabaseSettingsWidgetStatistics()) + , m_errIcon(FilePath::instance()->icon("status", "dialog-error")) +{ + m_ui->setupUi(this); +} + +DatabaseSettingsWidgetStatistics::~DatabaseSettingsWidgetStatistics() +{ +} + +void DatabaseSettingsWidgetStatistics::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 DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer db) +{ + m_referencesModel.reset(new QStandardItemModel()); + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Value")); + + const auto stats = Stats(db); + addStatsRow(tr("Database name"), db->metadata()->name()); + addStatsRow(tr("Description"), db->metadata()->description()); + addStatsRow(tr("Location"), db->filePath()); + addStatsRow(tr("Last saved"), stats.modified.toString(Qt::DefaultLocaleShortDate)); + addStatsRow(tr("Unsaved changes"), + db->isModified() ? tr("yes") : tr("no"), + 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("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.")); + + m_ui->sharedGroupsView->setModel(m_referencesModel.data()); + m_ui->sharedGroupsView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); +} + +void DatabaseSettingsWidgetStatistics::saveSettings() +{ + // nothing to do - the tab is passive +} diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h new file mode 100644 index 0000000000..189df41ffd --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +#ifndef KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H +#define KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H + +#include +#include + +class Database; +class QStandardItemModel; + +namespace Ui +{ + class DatabaseSettingsWidgetStatistics; +} + +class DatabaseSettingsWidgetStatistics : public QWidget +{ + Q_OBJECT +public: + explicit DatabaseSettingsWidgetStatistics(QWidget* parent = nullptr); + ~DatabaseSettingsWidgetStatistics(); + + void loadSettings(QSharedPointer db); + void saveSettings(); + +private: + QScopedPointer m_ui; + + QIcon m_errIcon; + QScopedPointer m_referencesModel; + + void addStatsRow(QString name, QString value, bool bad = false, QString badMsg = ""); +}; + +#endif // KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui new file mode 100644 index 0000000000..6eef5b366c --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui @@ -0,0 +1,79 @@ + + + DatabaseSettingsWidgetStatistics + + + + 0 + 0 + 327 + 379 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Statistics + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + Qt::ElideMiddle + + + false + + + false + + + true + + + false + + + + + + + + true + + + + Hover over lines with error icons for further information. + + + + + + + + + + +