diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 2ea2c0f72..4f11ecb83 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -7561,6 +7561,74 @@ Please consider generating a new key file.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QtIOCompressor
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a6dcd8e97..e9a07073f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -39,6 +39,7 @@ set(keepassx_SOURCES
core/Config.cpp
core/CustomData.cpp
core/Database.cpp
+ core/DatabaseStats.cpp
core/Entry.cpp
core/EntryAttachments.cpp
core/EntryAttributes.cpp
diff --git a/src/cli/Info.cpp b/src/cli/Info.cpp
index 4338cd000..bf093664a 100644
--- a/src/cli/Info.cpp
+++ b/src/cli/Info.cpp
@@ -18,7 +18,9 @@
#include "Info.h"
#include "Utils.h"
+#include "core/DatabaseStats.h"
#include "core/Global.h"
+#include "core/Group.h"
#include "core/Metadata.h"
#include
@@ -47,5 +49,25 @@ int Info::executeWithDatabase(QSharedPointer database, QSharedPointer<
} else {
out << QObject::tr("Recycle bin is not enabled.") << endl;
}
+
+ DatabaseStats stats(database);
+ out << QObject::tr("Location") << ": " << database->filePath() << endl;
+ out << QObject::tr("Database created") << ": "
+ << database->rootGroup()->timeInfo().creationTime().toString(Qt::DefaultLocaleShortDate) << endl;
+ out << QObject::tr("Last saved") << ": " << stats.modified.toString(Qt::DefaultLocaleShortDate) << endl;
+ out << QObject::tr("Unsaved changes") << ": " << (database->isModified() ? QObject::tr("yes") : QObject::tr("no"))
+ << endl;
+ out << QObject::tr("Number of groups") << ": " << QString::number(stats.groupCount) << endl;
+ out << QObject::tr("Number of entries") << ": " << QString::number(stats.entryCount) << endl;
+ out << QObject::tr("Number of expired entries") << ": " << QString::number(stats.expiredEntries) << endl;
+ out << QObject::tr("Unique passwords") << ": " << QString::number(stats.uniquePasswords) << endl;
+ out << QObject::tr("Non-unique passwords") << ": " << QString::number(stats.reusedPasswords) << endl;
+ out << QObject::tr("Maximum password reuse") << ": " << QString::number(stats.maxPwdReuse()) << endl;
+ out << QObject::tr("Number of short passwords") << ": " << QString::number(stats.shortPasswords) << endl;
+ out << QObject::tr("Number of weak passwords") << ": " << QString::number(stats.weakPasswords) << endl;
+ out << QObject::tr("Entries excluded from reports") << ": " << QString::number(stats.excludedEntries) << endl;
+ out << QObject::tr("Average password length") << ": " << QObject::tr("%1 characters").arg(stats.averagePwdLength())
+ << endl;
+
return EXIT_SUCCESS;
}
diff --git a/src/core/DatabaseStats.cpp b/src/core/DatabaseStats.cpp
new file mode 100644
index 000000000..cf2364b08
--- /dev/null
+++ b/src/core/DatabaseStats.cpp
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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 "DatabaseStats.h"
+
+// Ctor does all the work
+DatabaseStats::DatabaseStats(QSharedPointer db)
+ : modified(QFileInfo(db->filePath()).lastModified())
+ , m_db(db)
+{
+ gatherStats(db->rootGroup()->groupsRecursive(true));
+}
+
+// Get average password length
+int DatabaseStats::averagePwdLength() const
+{
+ const auto passwords = uniquePasswords + reusedPasswords;
+ return passwords == 0 ? 0 : std::round(totalPasswordLength / double(passwords));
+}
+
+// Get max number of password reuse (=how many entries
+// share the same password)
+int DatabaseStats::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 DatabaseStats::isAnyExpired() const
+{
+ return expiredEntries > 0;
+}
+
+bool DatabaseStats::areTooManyPwdsReused() const
+{
+ return reusedPasswords > uniquePasswords / 10;
+}
+
+bool DatabaseStats::arePwdsReusedTooOften() const
+{
+ return maxPwdReuse() > 3;
+}
+
+bool DatabaseStats::isAvgPwdTooShort() const
+{
+ return averagePwdLength() < 10;
+}
+
+void DatabaseStats::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;
+ }
+
+ ++groupCount;
+
+ for (const auto* entry : group->entries()) {
+ // Don't count anything in the recycle bin
+ if (entry->isRecycled()) {
+ continue;
+ }
+
+ ++entryCount;
+
+ if (entry->isExpired()) {
+ ++expiredEntries;
+ }
+
+ // Get password statistics
+ const auto pwd = entry->password();
+ if (!pwd.isEmpty()) {
+ if (!m_passwords.contains(pwd)) {
+ ++uniquePasswords;
+ } else {
+ ++reusedPasswords;
+ }
+
+ if (pwd.size() < PasswordHealth::Length::Short) {
+ ++shortPasswords;
+ }
+
+ // Speed up Zxcvbn process by excluding very long passwords and most passphrases
+ if (pwd.size() < PasswordHealth::Length::Long
+ && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
+ ++weakPasswords;
+ }
+
+ if (entry->excludeFromReports()) {
+ ++excludedEntries;
+ }
+
+ totalPasswordLength += pwd.size();
+ m_passwords[pwd]++;
+ }
+ }
+ }
+}
diff --git a/src/core/DatabaseStats.h b/src/core/DatabaseStats.h
new file mode 100644
index 000000000..2c0ad7c76
--- /dev/null
+++ b/src/core/DatabaseStats.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 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_DATABASESTATS_H
+#define KEEPASSXC_DATABASESTATS_H
+#include "PasswordHealth.h"
+#include "core/Group.h"
+#include
+#include
+class DatabaseStats
+{
+public:
+ // The statistics we collect:
+ QDateTime modified; // File modification time
+ 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
+
+ explicit DatabaseStats(QSharedPointer db);
+
+ int averagePwdLength() const;
+
+ int maxPwdReuse() const;
+
+ bool isAnyExpired() const;
+
+ bool areTooManyPwdsReused() const;
+
+ bool arePwdsReusedTooOften() const;
+
+ bool isAvgPwdTooShort() const;
+
+private:
+ QSharedPointer m_db;
+ QHash m_passwords;
+
+ void gatherStats(const QList& groups);
+};
+#endif // KEEPASSXC_DATABASESTATS_H
diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h
index 6e3179253..35b582e94 100644
--- a/src/core/PasswordHealth.h
+++ b/src/core/PasswordHealth.h
@@ -82,6 +82,12 @@ public:
return m_entropy;
}
+ struct Length
+ {
+ static const int Short = 8;
+ static const int Long = 25;
+ };
+
private:
int m_score = 0;
double m_entropy = 0.0;
diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp
index 3f37346eb..280ed4de5 100644
--- a/src/gui/reports/ReportsWidgetStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetStatistics.cpp
@@ -19,138 +19,14 @@
#include "ui_ReportsWidgetStatistics.h"
#include "core/AsyncTask.h"
+#include "core/DatabaseStats.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/PasswordHealth.h"
#include "gui/Icons.h"
-#include
#include
-namespace
-{
- class Stats
- {
- public:
- // The statistics we collect:
- QDateTime modified; // File modification time
- 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 db)
- : modified(QFileInfo(db->filePath()).lastModified())
- , m_db(db)
- {
- gatherStats(db->rootGroup()->groupsRecursive(true));
- }
-
- // Get average password length
- int averagePwdLength() const
- {
- const auto passwords = uniquePasswords + reusedPasswords;
- return passwords == 0 ? 0 : std::round(totalPasswordLength / double(passwords));
- }
-
- // 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 expiredEntries > 0;
- }
-
- bool areTooManyPwdsReused() const
- {
- return reusedPasswords > uniquePasswords / 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;
- }
-
- ++groupCount;
-
- for (const auto* entry : group->entries()) {
- // Don't count anything in the recycle bin
- if (entry->isRecycled()) {
- continue;
- }
-
- ++entryCount;
-
- if (entry->isExpired()) {
- ++expiredEntries;
- }
-
- // Get password statistics
- const auto pwd = entry->password();
- if (!pwd.isEmpty()) {
- if (!m_passwords.contains(pwd)) {
- ++uniquePasswords;
- } else {
- ++reusedPasswords;
- }
-
- if (pwd.size() < 8) {
- ++shortPasswords;
- }
-
- // Speed up Zxcvbn process by excluding very long passwords and most passphrases
- if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
- ++weakPasswords;
- }
-
- if (entry->excludeFromReports()) {
- ++excludedEntries;
- }
-
- totalPasswordLength += pwd.size();
- m_passwords[pwd]++;
- }
- }
- }
- }
- };
-} // namespace
-
ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ReportsWidgetStatistics())
@@ -205,7 +81,8 @@ void ReportsWidgetStatistics::showEvent(QShowEvent* event)
void ReportsWidgetStatistics::calculateStats()
{
- const QScopedPointer stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }));
+ const QScopedPointer stats(
+ AsyncTask::runAndWaitForFuture([this] { return new DatabaseStats(m_db); }));
m_referencesModel->clear();
addStatsRow(tr("Database name"), m_db->metadata()->name());
diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp
index 49b21dfa6..003d8fdbf 100644
--- a/tests/TestCli.cpp
+++ b/tests/TestCli.cpp
@@ -858,6 +858,22 @@ void TestCli::testInfo()
QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
+ QVERIFY(m_stdout->readLine().contains(m_dbFile->fileName().toUtf8()));
+ QVERIFY(m_stdout->readLine().contains(
+ QByteArray("Database created: "))); // date changes often, so just test for the first part
+ QVERIFY(m_stdout->readLine().contains(
+ QByteArray("Last saved: "))); // date changes often, so just test for the first part
+ QCOMPARE(m_stdout->readLine(), QByteArray("Unsaved changes: no\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Number of groups: 8\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Number of entries: 2\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Number of expired entries: 0\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Unique passwords: 2\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Non-unique passwords: 0\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Maximum password reuse: 1\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Number of short passwords: 0\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Number of weak passwords: 2\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Entries excluded from reports: 0\n"));
+ QCOMPARE(m_stdout->readLine(), QByteArray("Average password length: 11 characters\n"));
// Test with quiet option.
setInput("a");