diff --git a/share/demo.kdbx b/share/demo.kdbx index 71795676a..1f3727104 100644 Binary files a/share/demo.kdbx and b/share/demo.kdbx differ diff --git a/share/icons/application/scalable/actions/health.svg b/share/icons/application/scalable/actions/health.svg new file mode 100644 index 000000000..4cd5fa091 --- /dev/null +++ b/share/icons/application/scalable/actions/health.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index af9b9bb58..6b3d9abfa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -48,6 +48,7 @@ set(keepassx_SOURCES core/Merger.cpp core/Metadata.cpp core/PasswordGenerator.cpp + core/PasswordHealth.cpp core/PassphraseGenerator.cpp core/SignalMultiplexer.cpp core/ScreenLockListener.cpp @@ -149,8 +150,12 @@ set(keepassx_SOURCES gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp - gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp - gui/dbsettings/DatabaseSettingsPageStatistics.cpp + gui/reports/ReportsWidget.cpp + gui/reports/ReportsDialog.cpp + gui/reports/ReportsWidgetHealthcheck.cpp + gui/reports/ReportsPageHealthcheck.cpp + gui/reports/ReportsWidgetStatistics.cpp + gui/reports/ReportsPageStatistics.cpp gui/settings/SettingsWidget.cpp gui/widgets/ElidedLabel.cpp gui/widgets/PopupHelpWidget.cpp diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index 9cb4e0735..b49af7005 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -19,6 +19,7 @@ #include "BrowserSettings.h" #include "core/Config.h" +#include "core/PasswordHealth.h" BrowserSettings* BrowserSettings::m_instance(nullptr); @@ -541,7 +542,7 @@ QJsonObject BrowserSettings::generatePassword() m_passwordGenerator.setCharClasses(passwordCharClasses()); m_passwordGenerator.setFlags(passwordGeneratorFlags()); const QString pw = m_passwordGenerator.generatePassword(); - password["entropy"] = m_passwordGenerator.estimateEntropy(pw); + password["entropy"] = PasswordHealth(pw).entropy(); password["password"] = pw; } else { m_passPhraseGenerator.setWordCount(passPhraseWordCount()); diff --git a/src/cli/Estimate.cpp b/src/cli/Estimate.cpp index a84e23963..3b7509057 100644 --- a/src/cli/Estimate.cpp +++ b/src/cli/Estimate.cpp @@ -19,6 +19,7 @@ #include "cli/Utils.h" #include "cli/TextStream.h" +#include "core/PasswordHealth.h" #include #include #include @@ -49,10 +50,9 @@ static void estimate(const char* pwd, bool advanced) { TextStream out(Utils::STDOUT, QIODevice::WriteOnly); - double e = 0.0; int len = static_cast(strlen(pwd)); if (!advanced) { - e = ZxcvbnMatch(pwd, nullptr, nullptr); + const auto e = PasswordHealth(pwd).entropy(); // clang-format off out << QObject::tr("Length %1").arg(len, 0) << '\t' << QObject::tr("Entropy %1").arg(e, 0, 'f', 3) << '\t' @@ -62,7 +62,7 @@ static void estimate(const char* pwd, bool advanced) int ChkLen = 0; ZxcMatch_t *info, *p; double m = 0.0; - e = ZxcvbnMatch(pwd, nullptr, &info); + const auto e = ZxcvbnMatch(pwd, nullptr, &info); for (p = info; p; p = p->Next) { m += p->Entrpy; } diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp index e203af672..ff271a453 100644 --- a/src/core/PasswordGenerator.cpp +++ b/src/core/PasswordGenerator.cpp @@ -19,7 +19,6 @@ #include "PasswordGenerator.h" #include "crypto/Random.h" -#include const char* PasswordGenerator::DefaultExcludedChars = ""; @@ -31,11 +30,6 @@ PasswordGenerator::PasswordGenerator() { } -double PasswordGenerator::estimateEntropy(const QString& password) -{ - return ZxcvbnMatch(password.toLatin1(), nullptr, nullptr); -} - void PasswordGenerator::setLength(int length) { if (length <= 0) { diff --git a/src/core/PasswordGenerator.h b/src/core/PasswordGenerator.h index 22627d25b..55418b4ba 100644 --- a/src/core/PasswordGenerator.h +++ b/src/core/PasswordGenerator.h @@ -57,7 +57,6 @@ public: public: PasswordGenerator(); - double estimateEntropy(const QString& password); void setLength(int length); void setCharClasses(const CharClasses& classes); void setFlags(const GeneratorFlags& flags); diff --git a/src/core/PasswordHealth.cpp b/src/core/PasswordHealth.cpp new file mode 100644 index 000000000..58e4e42af --- /dev/null +++ b/src/core/PasswordHealth.cpp @@ -0,0 +1,188 @@ +/* + * 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 +#include + +#include "Database.h" +#include "Entry.h" +#include "Group.h" +#include "PasswordHealth.h" +#include "zxcvbn.h" + +PasswordHealth::PasswordHealth(double entropy) + : m_score(entropy) + , m_entropy(entropy) +{ + switch (quality()) { + case Quality::Bad: + case Quality::Poor: + m_scoreReasons << QApplication::tr("Very weak password"); + m_scoreDetails << QApplication::tr("Password entropy is %1 bits").arg(QString::number(m_entropy, 'f', 2)); + break; + + case Quality::Weak: + m_scoreReasons << QApplication::tr("Weak password"); + m_scoreDetails << QApplication::tr("Password entropy is %1 bits").arg(QString::number(m_entropy, 'f', 2)); + break; + + default: + // No reason or details for good and excellent passwords + break; + } +} + +PasswordHealth::PasswordHealth(QString pwd) + : PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr)) +{ +} + +void PasswordHealth::setScore(int score) +{ + m_score = score; +} + +void PasswordHealth::adjustScore(int amount) +{ + m_score += amount; +} + +QString PasswordHealth::scoreReason() const +{ + return m_scoreReasons.join("\n"); +} + +void PasswordHealth::addScoreReason(QString reason) +{ + m_scoreReasons << reason; +} + +QString PasswordHealth::scoreDetails() const +{ + return m_scoreDetails.join("\n"); +} + +void PasswordHealth::addScoreDetails(QString details) +{ + m_scoreDetails.append(details); +} + +PasswordHealth::Quality PasswordHealth::quality() const +{ + if (m_score <= 0) { + return Quality::Bad; + } else if (m_score < 40) { + return Quality::Poor; + } else if (m_score < 65) { + return Quality::Weak; + } else if (m_score < 100) { + return Quality::Good; + } + return Quality::Excellent; +} + +/** + * This class provides additional information about password health + * than can be derived from the password itself (re-use, expiry). + */ +HealthChecker::HealthChecker(QSharedPointer db) +{ + // Build the cache of re-used passwords + for (const auto* entry : db->rootGroup()->entriesRecursive()) { + if (!entry->isRecycled()) { + m_reuse[entry->password()] + << QApplication::tr("Used in %1/%2").arg(entry->group()->hierarchy().join('/'), entry->title()); + } + } +} + +/** + * Call operator of the Health Checker class. + * + * Returns the health of the password in `entry`, considering + * password entropy, re-use, expiration, etc. + */ +QSharedPointer HealthChecker::evaluate(const Entry* entry) +{ + if (!entry) { + return {}; + } + + // Return from cache if we saw it before + if (m_cache.contains(entry->uuid())) { + return m_cache[entry->uuid()]; + } + + // First analyse the password itself + const auto pwd = entry->password(); + auto health = QSharedPointer(new PasswordHealth(pwd)); + + // Second, if the password is in the database more than once, + // reduce the score accordingly + const auto& used = m_reuse[pwd]; + const auto count = used.size(); + if (count > 1) { + constexpr auto penalty = 15; + health->adjustScore(-penalty * (count - 1)); + health->addScoreReason(QApplication::tr("Password is used %1 times").arg(QString::number(count))); + // Add the first 20 uses of the password to prevent the details display from growing too large + for (int i = 0; i < used.size(); ++i) { + health->addScoreDetails(used[i]); + if (i == 19) { + health->addScoreDetails(QStringLiteral("...")); + break; + } + } + + // Don't allow re-used passwords to be considered "good" + // no matter how great their entropy is. + if (health->score() > 64) { + health->setScore(64); + } + } + + // Third, if the password has already expired, reduce score to 0; + // or, if the password is going to expire in the next 30 days, + // reduce score by 2 points per day. + if (entry->isExpired()) { + health->setScore(0); + health->addScoreReason(QApplication::tr("Password has expired")); + health->addScoreDetails(QApplication::tr("Password expiry was %1") + .arg(entry->timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate))); + } else if (entry->timeInfo().expires()) { + const auto days = QDateTime::currentDateTime().daysTo(entry->timeInfo().expiryTime()); + if (days <= 30) { + // First bring the score down into the "weak" range + // so that the entry appears in Health Check. Then + // reduce the score by 2 points for every day that + // we get closer to expiry. days<=0 has already + // been handled above ("isExpired()"). + if (health->score() > 60) { + health->setScore(60); + } + health->adjustScore((30 - days) * -2); + health->addScoreReason(days <= 2 ? QApplication::tr("Password is about to expire") + : days <= 10 ? QApplication::tr("Password expires in %1 days").arg(days) + : QApplication::tr("Password will expire soon")); + health->addScoreDetails(QApplication::tr("Password expires on %1") + .arg(entry->timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate))); + } + } + + // Return the result + return m_cache.insert(entry->uuid(), health).value(); +} diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h new file mode 100644 index 000000000..ca7f0236e --- /dev/null +++ b/src/core/PasswordHealth.h @@ -0,0 +1,113 @@ +/* + * 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 KEEPASSX_PASSWORDHEALTH_H +#define KEEPASSX_PASSWORDHEALTH_H + +#include +#include +#include + +class Database; +class Entry; + +/** + * Health status of a single password. + * + * @see HealthChecker + */ +class PasswordHealth +{ +public: + explicit PasswordHealth(double entropy); + explicit PasswordHealth(QString pwd); + + /* + * The password score is defined to be the greater the better + * (more secure) the password is. It doesn't have a dimension, + * there are no defined maximum or minimum values, and score + * values may change with different versions of the software. + */ + int score() const + { + return m_score; + } + + void setScore(int score); + void adjustScore(int amount); + + /* + * A text description for the password's quality assessment + * (translated into the application language), and additional + * information. Empty if nothing is wrong with the password. + * May contain more than line, separated by '\n'. + */ + QString scoreReason() const; + void addScoreReason(QString reason); + + QString scoreDetails() const; + void addScoreDetails(QString details); + + /* + * The password quality assessment (based on the score). + */ + enum class Quality + { + Bad, + Poor, + Weak, + Good, + Excellent + }; + Quality quality() const; + + /* + * The password's raw entropy value, in bits. + */ + double entropy() const + { + return m_entropy; + } + +private: + int m_score = 0; + double m_entropy = 0.0; + QStringList m_scoreReasons; + QStringList m_scoreDetails; +}; + +/** + * Password health check for all entries of a database. + * + * @see PasswordHealth + */ +class HealthChecker +{ +public: + explicit HealthChecker(QSharedPointer); + + // Get the health status of an entry in the database + QSharedPointer evaluate(const Entry* entry); + +private: + // Result cache (first=entry UUID) + QHash> m_cache; + // first = password, second = entries that use it + QHash m_reuse; +}; + +#endif // KEEPASSX_PASSWORDHEALTH_H diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 4b9fe5f85..bd24cf165 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -76,7 +76,7 @@ static const QString aboutContributors = R"(
  • fonic (Entry Table View)
  • kylemanna (YubiKey)
  • c4rlo (Offline HIBP Checker)
  • -
  • wolframroesler (HTML Exporter)
  • +
  • wolframroesler (HTML Export, Statistics, Password Health)
  • mdaniel (OpVault Importer)
  • keithbennett (KeePassHTTP)
  • Typz (KeePassHTTP)
  • diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index c37e6c5ea..7e158406b 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -457,6 +457,11 @@ void DatabaseTabWidget::changeMasterKey() currentDatabaseWidget()->switchToMasterKeyChange(); } +void DatabaseTabWidget::changeReports() +{ + currentDatabaseWidget()->switchToReports(); +} + void DatabaseTabWidget::changeDatabaseSettings() { currentDatabaseWidget()->switchToDatabaseSettings(); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 5c55bc63c..29019a2d2 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -78,6 +78,7 @@ public slots: void relockPendingDatabase(); void changeMasterKey(); + void changeReports(); void changeDatabaseSettings(); void performGlobalAutoType(); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index eb33c09c0..fd579b04a 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -59,6 +59,7 @@ #include "gui/entry/EntryView.h" #include "gui/group/EditGroupWidget.h" #include "gui/group/GroupView.h" +#include "gui/reports/ReportsDialog.h" #include "keeshare/KeeShare.h" #include "touchid/TouchID.h" @@ -88,6 +89,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) , m_editEntryWidget(new EditEntryWidget(this)) , m_editGroupWidget(new EditGroupWidget(this)) , m_historyEditEntryWidget(new EditEntryWidget(this)) + , m_reportsDialog(new ReportsDialog(this)) , m_databaseSettingDialog(new DatabaseSettingsDialog(this)) , m_databaseOpenWidget(new DatabaseOpenWidget(this)) , m_keepass1OpenWidget(new KeePass1OpenWidget(this)) @@ -165,6 +167,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) m_editEntryWidget->setObjectName("editEntryWidget"); m_editGroupWidget->setObjectName("editGroupWidget"); m_csvImportWizard->setObjectName("csvImportWizard"); + m_reportsDialog->setObjectName("reportsDialog"); m_databaseSettingDialog->setObjectName("databaseSettingsDialog"); m_databaseOpenWidget->setObjectName("databaseOpenWidget"); m_keepass1OpenWidget->setObjectName("keepass1OpenWidget"); @@ -173,6 +176,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) addChildWidget(m_mainWidget); addChildWidget(m_editEntryWidget); addChildWidget(m_editGroupWidget); + addChildWidget(m_reportsDialog); addChildWidget(m_databaseSettingDialog); addChildWidget(m_historyEditEntryWidget); addChildWidget(m_databaseOpenWidget); @@ -196,6 +200,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) connect(m_editEntryWidget, SIGNAL(historyEntryActivated(Entry*)), SLOT(switchToHistoryView(Entry*))); connect(m_historyEditEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchBackToEntryEdit())); connect(m_editGroupWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); + connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); @@ -1105,6 +1110,12 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod } } +void DatabaseWidget::switchToReports() +{ + m_reportsDialog->load(m_db); + setCurrentWidget(m_reportsDialog); +} + void DatabaseWidget::switchToDatabaseSettings() { m_databaseSettingDialog->load(m_db); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 9f0c5c976..6420a3b24 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -34,6 +34,7 @@ class DatabaseOpenWidget; class KeePass1OpenWidget; class OpVaultOpenWidget; class DatabaseSettingsDialog; +class ReportsDialog; class Database; class FileWatcher; class EditEntryWidget; @@ -181,6 +182,7 @@ public slots: void sortGroupsAsc(); void sortGroupsDesc(); void switchToMasterKeyChange(); + void switchToReports(); void switchToDatabaseSettings(); void switchToOpenDatabase(); void switchToOpenDatabase(const QString& filePath); @@ -251,6 +253,7 @@ private: QPointer m_editEntryWidget; QPointer m_editGroupWidget; QPointer m_historyEditEntryWidget; + QPointer m_reportsDialog; QPointer m_databaseSettingDialog; QPointer m_databaseOpenWidget; QPointer m_keepass1OpenWidget; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e9c150dd5..2d52331ff 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -332,6 +332,7 @@ MainWindow::MainWindow() m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save")); m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as")); m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close")); + m_ui->actionReports->setIcon(filePath()->icon("actions", "help-about")); m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit")); m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key")); m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "database-lock")); @@ -403,6 +404,7 @@ MainWindow::MainWindow() connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab())); connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase())); connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); + connect(m_ui->actionReports, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeReports())); connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); @@ -673,6 +675,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && currentGroupHasEntries && !recycleBinSelected); m_ui->actionChangeMasterKey->setEnabled(true); + m_ui->actionReports->setEnabled(true); m_ui->actionChangeDatabaseSettings->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); m_ui->actionDatabaseSaveAs->setEnabled(true); @@ -719,6 +722,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionReports->setEnabled(false); m_ui->actionChangeDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); @@ -746,6 +750,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionReports->setEnabled(false); m_ui->actionChangeDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index e09c91dd7..aec0efb37 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -236,6 +236,7 @@ + @@ -532,6 +533,20 @@ Change master &key... + + + false + + + &Reports... + + + Statistics, health check, etc. + + + QAction::NoRole + + false diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index e0f8fbe5f..c04487c0e 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -26,6 +26,7 @@ #include "core/Config.h" #include "core/FilePath.h" #include "core/PasswordGenerator.h" +#include "core/PasswordHealth.h" #include "gui/Clipboard.h" PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) @@ -261,21 +262,17 @@ void PasswordGeneratorWidget::updateButtonsEnabled(const QString& password) void PasswordGeneratorWidget::updatePasswordStrength(const QString& password) { - double entropy = 0.0; - if (m_ui->tabWidget->currentIndex() == Password) { - entropy = m_passwordGenerator->estimateEntropy(password); - } else { - entropy = m_dicewareGenerator->estimateEntropy(); + PasswordHealth health(password); + if (m_ui->tabWidget->currentIndex() == Diceware) { + // Diceware estimates entropy differently + health = PasswordHealth(m_dicewareGenerator->estimateEntropy()); } - m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(entropy, 'f', 2))); + m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(health.entropy(), 'f', 2))); - if (entropy > m_ui->entropyProgressBar->maximum()) { - entropy = m_ui->entropyProgressBar->maximum(); - } - m_ui->entropyProgressBar->setValue(entropy); + m_ui->entropyProgressBar->setValue(std::min(int(health.entropy()), m_ui->entropyProgressBar->maximum())); - colorStrengthIndicator(entropy); + colorStrengthIndicator(health); } void PasswordGeneratorWidget::applyPassword() @@ -384,7 +381,7 @@ void PasswordGeneratorWidget::excludeHexChars() m_ui->editExcludedChars->setText("GHIJKLMNOPQRSTUVWXYZghijklmnopqrstuvwxyz"); } -void PasswordGeneratorWidget::colorStrengthIndicator(double entropy) +void PasswordGeneratorWidget::colorStrengthIndicator(const PasswordHealth& health) { // Take the existing stylesheet and convert the text and background color to arguments QString style = m_ui->entropyProgressBar->styleSheet(); @@ -395,18 +392,27 @@ void PasswordGeneratorWidget::colorStrengthIndicator(double entropy) // Set the color and background based on entropy // colors are taking from the KDE breeze palette // - if (entropy < 40) { + switch (health.quality()) { + case PasswordHealth::Quality::Bad: + case PasswordHealth::Quality::Poor: m_ui->entropyProgressBar->setStyleSheet(style.arg("#c0392b")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Poor", "Password quality"))); - } else if (entropy >= 40 && entropy < 65) { + break; + + case PasswordHealth::Quality::Weak: m_ui->entropyProgressBar->setStyleSheet(style.arg("#f39c1f")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Weak", "Password quality"))); - } else if (entropy >= 65 && entropy < 100) { + break; + + case PasswordHealth::Quality::Good: m_ui->entropyProgressBar->setStyleSheet(style.arg("#11d116")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Good", "Password quality"))); - } else { + break; + + case PasswordHealth::Quality::Excellent: m_ui->entropyProgressBar->setStyleSheet(style.arg("#27ae60")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Excellent", "Password quality"))); + break; } } diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index b39a2f10f..eba7f815f 100644 --- a/src/gui/PasswordGeneratorWidget.h +++ b/src/gui/PasswordGeneratorWidget.h @@ -32,6 +32,7 @@ namespace Ui } class PasswordGenerator; +class PasswordHealth; class PassphraseGenerator; class PasswordGeneratorWidget : public QWidget @@ -77,7 +78,7 @@ private slots: void passwordSpinBoxChanged(); void dicewareSliderMoved(); void dicewareSpinBoxChanged(); - void colorStrengthIndicator(double entropy); + void colorStrengthIndicator(const PasswordHealth& health); void updateGenerator(); diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index 33c4df2c4..e0e6765a4 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -19,7 +19,6 @@ #include "DatabaseSettingsDialog.h" #include "ui_DatabaseSettingsDialog.h" -#include "DatabaseSettingsPageStatistics.h" #include "DatabaseSettingsWidgetEncryption.h" #include "DatabaseSettingsWidgetGeneral.h" #include "DatabaseSettingsWidgetMasterKey.h" @@ -85,8 +84,6 @@ 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/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp new file mode 100644 index 000000000..22ebab41a --- /dev/null +++ b/src/gui/reports/ReportsDialog.cpp @@ -0,0 +1,128 @@ +/* + * 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 "ReportsDialog.h" +#include "ui_ReportsDialog.h" + +#include "ReportsPageHealthcheck.h" +#include "ReportsPageStatistics.h" +#include "ReportsWidgetHealthcheck.h" + +#include "core/Global.h" +#include "touchid/TouchID.h" +#include +#include + +class ReportsDialog::ExtraPage +{ +public: + ExtraPage(QSharedPointer p, QWidget* w) + : page(p) + , widget(w) + { + } + void loadSettings(QSharedPointer db) const + { + page->loadSettings(widget, db); + } + void saveSettings() const + { + page->saveSettings(widget); + } + +private: + QSharedPointer page; + QWidget* widget; +}; + +ReportsDialog::ReportsDialog(QWidget* parent) + : DialogyWidget(parent) + , m_ui(new Ui::ReportsDialog()) + , m_healthPage(new ReportsPageHealthcheck()) + , m_statPage(new ReportsPageStatistics()) + , m_editEntryWidget(new EditEntryWidget(this)) +{ + m_ui->setupUi(this); + + connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); + addPage(m_healthPage); + addPage(m_statPage); + + m_ui->stackedWidget->setCurrentIndex(0); + + m_editEntryWidget->setObjectName("editEntryWidget"); + m_editEntryWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + m_ui->stackedWidget->addWidget(m_editEntryWidget); + adjustSize(); + + connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int))); + connect(m_healthPage->m_healthWidget, + SIGNAL(entryActivated(const Group*, Entry*)), + SLOT(entryActivationSignalReceived(const Group*, Entry*))); + connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); +} + +ReportsDialog::~ReportsDialog() +{ +} + +void ReportsDialog::load(const QSharedPointer& db) +{ + m_ui->categoryList->setCurrentCategory(0); + for (const ExtraPage& page : asConst(m_extraPages)) { + page.loadSettings(db); + } + m_db = db; +} + +void ReportsDialog::addPage(QSharedPointer page) +{ + const auto category = m_ui->categoryList->currentCategory(); + const auto widget = page->createWidget(); + widget->setParent(this); + m_extraPages.append(ExtraPage(page, widget)); + m_ui->stackedWidget->addWidget(widget); + m_ui->categoryList->addCategory(page->name(), page->icon()); + m_ui->categoryList->setCurrentCategory(category); +} + +void ReportsDialog::reject() +{ + for (const ExtraPage& extraPage : asConst(m_extraPages)) { + extraPage.saveSettings(); + } + +#ifdef WITH_XC_TOUCHID + TouchID::getInstance().reset(m_db ? m_db->filePath() : ""); +#endif + + emit editFinished(true); +} + +void ReportsDialog::entryActivationSignalReceived(const Group* group, Entry* entry) +{ + m_editEntryWidget->loadEntry(entry, false, false, group->hierarchy().join(" > "), m_db); + m_ui->stackedWidget->setCurrentWidget(m_editEntryWidget); +} + +void ReportsDialog::switchToMainView(bool previousDialogAccepted) +{ + m_ui->stackedWidget->setCurrentWidget(m_healthPage->m_healthWidget); + if (previousDialogAccepted) { + m_healthPage->m_healthWidget->calculateHealth(); + } +} diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h new file mode 100644 index 000000000..7a53623c3 --- /dev/null +++ b/src/gui/reports/ReportsDialog.h @@ -0,0 +1,85 @@ +/* + * 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 KEEPASSX_REPORTSWIDGET_H +#define KEEPASSX_REPORTSWIDGET_H + +#include "config-keepassx.h" +#include "gui/DialogyWidget.h" +#include "gui/entry/EditEntryWidget.h" + +#include +#include +#include + +class Database; +class Entry; +class Group; +class QTabWidget; +class ReportsPageHealthcheck; +class ReportsPageStatistics; + +namespace Ui +{ + class ReportsDialog; +} + +class IReportsPage +{ +public: + virtual ~IReportsPage() + { + } + virtual QString name() = 0; + virtual QIcon icon() = 0; + virtual QWidget* createWidget() = 0; + virtual void loadSettings(QWidget* widget, QSharedPointer db) = 0; + virtual void saveSettings(QWidget* widget) = 0; +}; + +class ReportsDialog : public DialogyWidget +{ + Q_OBJECT + +public: + explicit ReportsDialog(QWidget* parent = nullptr); + ~ReportsDialog() override; + Q_DISABLE_COPY(ReportsDialog); + + void load(const QSharedPointer& db); + void addPage(QSharedPointer page); + +signals: + void editFinished(bool accepted); + +private slots: + void reject(); + void entryActivationSignalReceived(const Group*, Entry* entry); + void switchToMainView(bool previousDialogAccepted); + +private: + QSharedPointer m_db; + const QScopedPointer m_ui; + const QSharedPointer m_healthPage; + const QSharedPointer m_statPage; + QPointer m_editEntryWidget; + + class ExtraPage; + QList m_extraPages; +}; + +#endif // KEEPASSX_REPORTSWIDGET_H diff --git a/src/gui/reports/ReportsDialog.ui b/src/gui/reports/ReportsDialog.ui new file mode 100644 index 000000000..773981a10 --- /dev/null +++ b/src/gui/reports/ReportsDialog.ui @@ -0,0 +1,43 @@ + + + ReportsDialog + + + + + + + + + + + -1 + + + + + + + + + + + QDialogButtonBox::Close + + + + + + + + + + CategoryListWidget + QWidget +
    gui/CategoryListWidget.h
    + 1 +
    +
    + + +
    diff --git a/src/gui/reports/ReportsPageHealthcheck.cpp b/src/gui/reports/ReportsPageHealthcheck.cpp new file mode 100644 index 000000000..41fa40625 --- /dev/null +++ b/src/gui/reports/ReportsPageHealthcheck.cpp @@ -0,0 +1,55 @@ +/* + * 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 "ReportsPageHealthcheck.h" + +#include "ReportsWidgetHealthcheck.h" +#include "core/FilePath.h" + +#include + +ReportsPageHealthcheck::ReportsPageHealthcheck() + : m_healthWidget(new ReportsWidgetHealthcheck()) +{ +} + +QString ReportsPageHealthcheck::name() +{ + return QApplication::tr("Health Check"); +} + +QIcon ReportsPageHealthcheck::icon() +{ + return FilePath::instance()->icon("actions", "health"); +} + +QWidget* ReportsPageHealthcheck::createWidget() +{ + return m_healthWidget; +} + +void ReportsPageHealthcheck::loadSettings(QWidget* widget, QSharedPointer db) +{ + const auto settingsWidget = reinterpret_cast(widget); + settingsWidget->loadSettings(db); +} + +void ReportsPageHealthcheck::saveSettings(QWidget* widget) +{ + const auto settingsWidget = reinterpret_cast(widget); + settingsWidget->saveSettings(); +} diff --git a/src/gui/reports/ReportsPageHealthcheck.h b/src/gui/reports/ReportsPageHealthcheck.h new file mode 100644 index 000000000..8a85b2d20 --- /dev/null +++ b/src/gui/reports/ReportsPageHealthcheck.h @@ -0,0 +1,41 @@ +/* + * 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_REPORTSPAGEHEALTHCHECK_H +#define KEEPASSXC_REPORTSPAGEHEALTHCHECK_H + +#include + +#include "ReportsDialog.h" + +class ReportsWidgetHealthcheck; + +class ReportsPageHealthcheck : public IReportsPage +{ +public: + ReportsWidgetHealthcheck* m_healthWidget; + + ReportsPageHealthcheck(); + + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget, QSharedPointer db) override; + void saveSettings(QWidget* widget) override; +}; + +#endif // KEEPASSXC_REPORTSPAGEHEALTHCHECK_H diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp b/src/gui/reports/ReportsPageStatistics.cpp similarity index 57% rename from src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp rename to src/gui/reports/ReportsPageStatistics.cpp index 6fe24ff0f..e4570e172 100644 --- a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp +++ b/src/gui/reports/ReportsPageStatistics.cpp @@ -15,38 +15,36 @@ * along with this program. If not, see . */ -#include "DatabaseSettingsPageStatistics.h" +#include "ReportsPageStatistics.h" -#include "DatabaseSettingsWidgetStatistics.h" -#include "core/Database.h" +#include "ReportsWidgetStatistics.h" #include "core/FilePath.h" -#include "core/Group.h" #include -QString DatabaseSettingsPageStatistics::name() +QString ReportsPageStatistics::name() { return QApplication::tr("Statistics"); } -QIcon DatabaseSettingsPageStatistics::icon() +QIcon ReportsPageStatistics::icon() { return FilePath::instance()->icon("actions", "statistics"); } -QWidget* DatabaseSettingsPageStatistics::createWidget() +QWidget* ReportsPageStatistics::createWidget() { - return new DatabaseSettingsWidgetStatistics(); + return new ReportsWidgetStatistics(); } -void DatabaseSettingsPageStatistics::loadSettings(QWidget* widget, QSharedPointer db) +void ReportsPageStatistics::loadSettings(QWidget* widget, QSharedPointer db) { - DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast(widget); + ReportsWidgetStatistics* settingsWidget = reinterpret_cast(widget); settingsWidget->loadSettings(db); } -void DatabaseSettingsPageStatistics::saveSettings(QWidget* widget) +void ReportsPageStatistics::saveSettings(QWidget* widget) { - DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast(widget); + ReportsWidgetStatistics* settingsWidget = reinterpret_cast(widget); settingsWidget->saveSettings(); } diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h b/src/gui/reports/ReportsPageStatistics.h similarity index 78% rename from src/gui/dbsettings/DatabaseSettingsPageStatistics.h rename to src/gui/reports/ReportsPageStatistics.h index c890f3b81..00d611ee3 100644 --- a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h +++ b/src/gui/reports/ReportsPageStatistics.h @@ -15,14 +15,14 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H -#define KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H +#ifndef KEEPASSXC_REPORTSPAGESTATISTICS_H +#define KEEPASSXC_REPORTSPAGESTATISTICS_H #include -#include "DatabaseSettingsDialog.h" +#include "ReportsDialog.h" -class DatabaseSettingsPageStatistics : public IDatabaseSettingsPage +class ReportsPageStatistics : public IReportsPage { public: QString name() override; @@ -32,4 +32,4 @@ public: void saveSettings(QWidget* widget) override; }; -#endif // KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H +#endif // KEEPASSXC_REPORTSPAGESTATISTICS_H diff --git a/src/gui/reports/ReportsWidget.cpp b/src/gui/reports/ReportsWidget.cpp new file mode 100644 index 000000000..184434116 --- /dev/null +++ b/src/gui/reports/ReportsWidget.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 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 "ReportsWidget.h" + +ReportsWidget::ReportsWidget(QWidget* parent) + : SettingsWidget(parent) +{ +} + +ReportsWidget::~ReportsWidget() +{ +} + +/** + * Load the database to be configured by this page and initialize the page. + * The page will NOT take ownership of the database. + * + * @param db database object to be configured + */ +void ReportsWidget::load(QSharedPointer db) +{ + m_db = std::move(db); + initialize(); +} + +const QSharedPointer ReportsWidget::getDatabase() const +{ + return m_db; +} diff --git a/src/gui/reports/ReportsWidget.h b/src/gui/reports/ReportsWidget.h new file mode 100644 index 000000000..631490405 --- /dev/null +++ b/src/gui/reports/ReportsWidget.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 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_REPORTSWIDGET_H +#define KEEPASSXC_REPORTSWIDGET_H + +#include "gui/settings/SettingsWidget.h" + +#include + +class Database; + +/** + * Pure-virtual base class for KeePassXC database settings widgets. + */ +class ReportsWidget : public SettingsWidget +{ + Q_OBJECT + +public: + explicit ReportsWidget(QWidget* parent = nullptr); + Q_DISABLE_COPY(ReportsWidget); + ~ReportsWidget() override; + + virtual void load(QSharedPointer db); + + const QSharedPointer getDatabase() const; + +signals: + /** + * Can be emitted to indicate size changes and allow parents widgets to adjust properly. + */ + void sizeChanged(); + +protected: + QSharedPointer m_db; +}; + +#endif // KEEPASSXC_REPORTSWIDGET_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp new file mode 100644 index 000000000..c668b3495 --- /dev/null +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -0,0 +1,237 @@ +/* + * 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 "ReportsWidgetHealthcheck.h" +#include "ui_ReportsWidgetHealthcheck.h" + +#include "core/AsyncTask.h" +#include "core/Database.h" +#include "core/FilePath.h" +#include "core/Group.h" +#include "core/PasswordHealth.h" + +#include +#include +#include + +namespace +{ + class Health + { + public: + struct Item + { + QPointer group; + QPointer entry; + QSharedPointer health; + + Item(const Group* g, const Entry* e, QSharedPointer h) + : group(g) + , entry(e) + , health(h) + { + } + + bool operator<(const Item& rhs) const + { + return health->score() < rhs.health->score(); + } + }; + + explicit Health(QSharedPointer); + + const QList>& items() const + { + return m_items; + } + + private: + QSharedPointer m_db; + HealthChecker m_checker; + QList> m_items; + }; +} // namespace + +Health::Health(QSharedPointer db) + : m_db(db) + , m_checker(db) +{ + for (const auto* group : db->rootGroup()->groupsRecursive(true)) { + // Skip recycle bin + if (group->isRecycled()) { + continue; + } + + for (const auto* entry : group->entries()) { + if (entry->isRecycled()) { + continue; + } + + // Skip entries with empty password + if (entry->password().isEmpty()) { + continue; + } + + // Add entry if its password isn't at least "good" + const auto item = QSharedPointer(new Item(group, entry, m_checker.evaluate(entry))); + if (item->health->quality() < PasswordHealth::Quality::Good) { + m_items.append(item); + } + } + } + + // Sort the result so that the worst passwords (least score) + // are at the top + std::sort(m_items.begin(), m_items.end(), [](QSharedPointer x, QSharedPointer y) { return *x < *y; }); +} + +ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::ReportsWidgetHealthcheck()) + , m_errorIcon(FilePath::instance()->icon("status", "dialog-error")) +{ + m_ui->setupUi(this); + + m_referencesModel.reset(new QStandardItemModel()); + m_ui->healthcheckTableView->setModel(m_referencesModel.data()); + m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection); + m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); +} + +ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() +{ +} + +void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer health, + const Group* group, + const Entry* entry) +{ + QString descr, tip; + QColor qualityColor; + const auto quality = health->quality(); + switch (quality) { + case PasswordHealth::Quality::Bad: + descr = tr("Bad", "Password quality"); + tip = tr("Bad — password must be changed"); + qualityColor.setNamedColor("red"); + break; + + case PasswordHealth::Quality::Poor: + descr = tr("Poor", "Password quality"); + tip = tr("Poor — password should be changed"); + qualityColor.setNamedColor("orange"); + break; + + case PasswordHealth::Quality::Weak: + descr = tr("Weak", "Password quality"); + tip = tr("Weak — consider changing the password"); + qualityColor.setNamedColor("yellow"); + break; + + case PasswordHealth::Quality::Good: + case PasswordHealth::Quality::Excellent: + qualityColor.setNamedColor("green"); + break; + } + + auto row = QList(); + row << new QStandardItem(descr); + row << new QStandardItem(entry->iconPixmap(), entry->title()); + row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/")); + row << new QStandardItem(QString::number(health->score())); + row << new QStandardItem(health->scoreReason()); + + // Set background color of first column according to password quality. + // Set the same as foreground color so the description is usually + // invisible, it's just for screen readers etc. + QBrush brush(qualityColor); + row[0]->setForeground(brush); + row[0]->setBackground(brush); + + // Set tooltips + row[0]->setToolTip(tip); + row[4]->setToolTip(health->scoreDetails()); + + // Store entry pointer per table row (used in double click handler) + m_referencesModel->appendRow(row); + m_rowToEntry.append({group, entry}); +} + +void ReportsWidgetHealthcheck::loadSettings(QSharedPointer db) +{ + m_db = std::move(db); + m_healthCalculated = false; + m_referencesModel->clear(); + m_rowToEntry.clear(); + + auto row = QList(); + row << new QStandardItem(tr("Please wait, health data is being calculated...")); + m_referencesModel->appendRow(row); +} + +void ReportsWidgetHealthcheck::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + + if (!m_healthCalculated) { + // Perform stats calculation on next event loop to allow widget to appear + m_healthCalculated = true; + QTimer::singleShot(0, this, SLOT(calculateHealth())); + } +} + +void ReportsWidgetHealthcheck::calculateHealth() +{ + m_referencesModel->clear(); + + const QScopedPointer health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); })); + if (health->items().empty()) { + // No findings + m_referencesModel->clear(); + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, everything is healthy!")); + } else { + // Show our findings + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score") + << tr("Reason")); + for (const auto& item : health->items()) { + addHealthRow(item->health, item->group, item->entry); + } + } + + m_ui->healthcheckTableView->resizeRowsToContents(); +} + +void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) +{ + if (!index.isValid()) { + return; + } + + const auto row = m_rowToEntry[index.row()]; + const auto group = row.first; + const auto entry = row.second; + if (group && entry) { + emit entryActivated(group, const_cast(entry)); + } +} + +void ReportsWidgetHealthcheck::saveSettings() +{ + // nothing to do - the tab is passive +} diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h new file mode 100644 index 000000000..bf0cf531e --- /dev/null +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -0,0 +1,70 @@ +/* + * 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_REPORTSWIDGETHEALTHCHECK_H +#define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H + +#include "gui/entry/EntryModel.h" +#include +#include +#include +#include + +class Database; +class Entry; +class Group; +class PasswordHealth; +class QStandardItemModel; + +namespace Ui +{ + class ReportsWidgetHealthcheck; +} + +class ReportsWidgetHealthcheck : public QWidget +{ + Q_OBJECT +public: + explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr); + ~ReportsWidgetHealthcheck(); + + void loadSettings(QSharedPointer db); + void saveSettings(); + +protected: + void showEvent(QShowEvent* event) override; + +signals: + void entryActivated(const Group* group, Entry* entry); + +public slots: + void calculateHealth(); + void emitEntryActivated(const QModelIndex& index); + +private: + void addHealthRow(QSharedPointer, const Group*, const Entry*); + + QScopedPointer m_ui; + + bool m_healthCalculated = false; + QIcon m_errorIcon; + QScopedPointer m_referencesModel; + QSharedPointer m_db; + QList> m_rowToEntry; +}; + +#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui new file mode 100644 index 000000000..48d8df07f --- /dev/null +++ b/src/gui/reports/ReportsWidgetHealthcheck.ui @@ -0,0 +1,79 @@ + + + ReportsWidgetHealthcheck + + + + 0 + 0 + 327 + 379 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Health Check + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + Qt::ElideMiddle + + + false + + + true + + + true + + + false + + + + + + + + true + + + + Hover over reason to show additional details. Double-click entries to edit. + + + + + + + + + + + diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp similarity index 86% rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp rename to src/gui/reports/ReportsWidgetStatistics.cpp index b02741adb..bc642af78 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -15,15 +15,15 @@ * along with this program. If not, see . */ -#include "DatabaseSettingsWidgetStatistics.h" -#include "ui_DatabaseSettingsWidgetStatistics.h" +#include "ReportsWidgetStatistics.h" +#include "ui_ReportsWidgetStatistics.h" #include "core/AsyncTask.h" #include "core/Database.h" #include "core/FilePath.h" #include "core/Group.h" #include "core/Metadata.h" -#include "zxcvbn.h" +#include "core/PasswordHealth.h" #include #include @@ -48,6 +48,7 @@ namespace // Ctor does all the work explicit Stats(QSharedPointer db) : modified(QFileInfo(db->filePath()).lastModified()) + , m_db(db) { gatherStats(db->rootGroup()->groupsRecursive(true)); } @@ -92,19 +93,27 @@ namespace } 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 == group->database()->metadata()->recycleBin()) { + 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()) { @@ -125,7 +134,7 @@ namespace } // Speed up Zxcvbn process by excluding very long passwords and most passphrases - if (pwd.size() < 25 && ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr) < 65) { + if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) { ++nPwdsWeak; } @@ -138,9 +147,9 @@ namespace }; } // namespace -DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* parent) +ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent) : QWidget(parent) - , m_ui(new Ui::DatabaseSettingsWidgetStatistics()) + , m_ui(new Ui::ReportsWidgetStatistics()) , m_errIcon(FilePath::instance()->icon("status", "dialog-error")) { m_ui->setupUi(this); @@ -148,14 +157,15 @@ DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* pare 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); } -DatabaseSettingsWidgetStatistics::~DatabaseSettingsWidgetStatistics() +ReportsWidgetStatistics::~ReportsWidgetStatistics() { } -void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg) +void ReportsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg) { auto row = QList(); row << new QStandardItem(name); @@ -170,7 +180,7 @@ void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value, } }; -void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer db) +void ReportsWidgetStatistics::loadSettings(QSharedPointer db) { m_db = std::move(db); m_statsCalculated = false; @@ -178,7 +188,7 @@ void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer db) addStatsRow(tr("Please wait, database statistics are being calculated..."), ""); } -void DatabaseSettingsWidgetStatistics::showEvent(QShowEvent* event) +void ReportsWidgetStatistics::showEvent(QShowEvent* event) { QWidget::showEvent(event); @@ -189,9 +199,9 @@ void DatabaseSettingsWidgetStatistics::showEvent(QShowEvent* event) } } -void DatabaseSettingsWidgetStatistics::calculateStats() +void ReportsWidgetStatistics::calculateStats() { - const auto stats = AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }); + const QScopedPointer stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); })); m_referencesModel->clear(); addStatsRow(tr("Database name"), m_db->metadata()->name()); @@ -231,7 +241,7 @@ void DatabaseSettingsWidgetStatistics::calculateStats() tr("Average password length is less than ten characters. Longer passwords provide more security.")); } -void DatabaseSettingsWidgetStatistics::saveSettings() +void ReportsWidgetStatistics::saveSettings() { // nothing to do - the tab is passive } diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h b/src/gui/reports/ReportsWidgetStatistics.h similarity index 74% rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h rename to src/gui/reports/ReportsWidgetStatistics.h index 2bd42f13d..cc11a75f5 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h +++ b/src/gui/reports/ReportsWidgetStatistics.h @@ -15,8 +15,8 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H -#define KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H +#ifndef KEEPASSXC_REPORTSWIDGETSTATISTICS_H +#define KEEPASSXC_REPORTSWIDGETSTATISTICS_H #include #include @@ -26,15 +26,15 @@ class QStandardItemModel; namespace Ui { - class DatabaseSettingsWidgetStatistics; + class ReportsWidgetStatistics; } -class DatabaseSettingsWidgetStatistics : public QWidget +class ReportsWidgetStatistics : public QWidget { Q_OBJECT public: - explicit DatabaseSettingsWidgetStatistics(QWidget* parent = nullptr); - ~DatabaseSettingsWidgetStatistics(); + explicit ReportsWidgetStatistics(QWidget* parent = nullptr); + ~ReportsWidgetStatistics(); void loadSettings(QSharedPointer db); void saveSettings(); @@ -46,7 +46,7 @@ private slots: void calculateStats(); private: - QScopedPointer m_ui; + QScopedPointer m_ui; bool m_statsCalculated = false; QIcon m_errIcon; @@ -56,4 +56,4 @@ private: void addStatsRow(QString name, QString value, bool bad = false, QString badMsg = ""); }; -#endif // KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H +#endif // KEEPASSXC_REPORTSWIDGETSTATISTICS_H diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui b/src/gui/reports/ReportsWidgetStatistics.ui similarity index 94% rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui rename to src/gui/reports/ReportsWidgetStatistics.ui index ed9d6346e..1f3bf5fea 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui +++ b/src/gui/reports/ReportsWidgetStatistics.ui @@ -1,7 +1,7 @@ - DatabaseSettingsWidgetStatistics - + ReportsWidgetStatistics + 0 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fc27f48d3..c3f1c0e22 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -176,6 +176,9 @@ add_unit_test(NAME testmerge SOURCES TestMerge.cpp add_unit_test(NAME testpasswordgenerator SOURCES TestPasswordGenerator.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testpasswordhealth SOURCES TestPasswordHealth.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testpassphrasegenerator SOURCES TestPassphraseGenerator.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestPasswordHealth.cpp b/tests/TestPasswordHealth.cpp new file mode 100644 index 000000000..238b78b92 --- /dev/null +++ b/tests/TestPasswordHealth.cpp @@ -0,0 +1,65 @@ +/* + * 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 "TestPasswordHealth.h" +#include "TestGlobal.h" + +#include "core/PasswordHealth.h" + +QTEST_GUILESS_MAIN(TestPasswordHealth) + +void TestPasswordHealth::initTestCase() +{ +} + +void TestPasswordHealth::testNoDb() +{ + const auto empty = PasswordHealth(""); + QCOMPARE(empty.score(), 0); + QCOMPARE(empty.entropy(), 0.0); + QCOMPARE(empty.quality(), PasswordHealth::Quality::Bad); + QVERIFY(!empty.scoreReason().isEmpty()); + QVERIFY(!empty.scoreDetails().isEmpty()); + + const auto poor = PasswordHealth("secret"); + QCOMPARE(poor.score(), 6); + QCOMPARE(int(poor.entropy()), 6); + QCOMPARE(poor.quality(), PasswordHealth::Quality::Poor); + QVERIFY(!poor.scoreReason().isEmpty()); + QVERIFY(!poor.scoreDetails().isEmpty()); + + const auto weak = PasswordHealth("Yohb2ChR4"); + QCOMPARE(weak.score(), 47); + QCOMPARE(int(weak.entropy()), 47); + QCOMPARE(weak.quality(), PasswordHealth::Quality::Weak); + QVERIFY(!weak.scoreReason().isEmpty()); + QVERIFY(!weak.scoreDetails().isEmpty()); + + const auto good = PasswordHealth("MIhIN9UKrgtPL2hp"); + QCOMPARE(good.score(), 78); + QCOMPARE(int(good.entropy()), 78); + QCOMPARE(good.quality(), PasswordHealth::Quality::Good); + QVERIFY(good.scoreReason().isEmpty()); + QVERIFY(good.scoreDetails().isEmpty()); + + const auto excellent = PasswordHealth("prompter-ream-oversleep-step-extortion-quarrel-reflected-prefix"); + QCOMPARE(excellent.score(), 164); + QCOMPARE(int(excellent.entropy()), 164); + QCOMPARE(excellent.quality(), PasswordHealth::Quality::Excellent); + QVERIFY(excellent.scoreReason().isEmpty()); + QVERIFY(excellent.scoreDetails().isEmpty()); +} diff --git a/tests/TestPasswordHealth.h b/tests/TestPasswordHealth.h new file mode 100644 index 000000000..2d887a7de --- /dev/null +++ b/tests/TestPasswordHealth.h @@ -0,0 +1,32 @@ +/* + * 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 KEEPASSX_TESTPASSWORDHEALTH_H +#define KEEPASSX_TESTPASSWORDHEALTH_H + +#include + +class TestPasswordHealth : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testNoDb(); +}; + +#endif // KEEPASSX_TESTPASSWORDHEALTH_H diff --git a/utils/makeicons.sh b/utils/makeicons.sh index 6efc608ee..887874161 100644 --- a/utils/makeicons.sh +++ b/utils/makeicons.sh @@ -99,6 +99,7 @@ map() { group-edit) echo folder-edit-outline ;; group-empty-trash) echo trash-can-outline ;; group-new) echo folder-plus-outline ;; + health) echo heart-pulse ;; help-about) echo information-outline ;; internet-web-browser) echo web ;; key-enter) echo keyboard-variant ;;