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. Use custom character set + + Location + + + + Database created + + + + Last saved + + + + Unsaved changes + + + + yes + + + + no + + + + Number of groups + + + + Number of entries + + + + Number of expired entries + + + + Unique passwords + + + + Non-unique passwords + + + + Maximum password reuse + + + + Number of short passwords + + + + Number of weak passwords + + + + Entries excluded from reports + + + + Average password length + + + + %1 characters + + 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");