Implement Password Health Report

Introduce a password health check to the application that evaluates every entry in a database. Entries that fail  various tests are listed for user review and action. Also moves the statistics panel to the new Database -> Reports  widget. Recycled entries are excluded from the results.

We now have two classes, PasswordHealth to deal with a single password and HealthChecker to deal with all passwords of a database.

Tests include passwords that are expired, re-used, and weak.

* Closes #551

* Move zxcvbn usage to a centralized class (PasswordHealth)  and replace its usages across the application to ensure standardized interpretation of entropy calculations.

* Add new icons for the database reports view

* Updated the demo database to show off the reports
This commit is contained in:
Wolfram Rösler 2020-02-01 08:42:34 -05:00 committed by Jonathan White
parent 71a39c37ec
commit a81c6469a8
38 changed files with 1364 additions and 75 deletions

Binary file not shown.

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-heart-pulse" width="24" height="24" viewBox="0 0 24 24"><path d="M7.5,4A5.5,5.5 0 0,0 2,9.5C2,10 2.09,10.5 2.22,11H6.3L7.57,7.63C7.87,6.83 9.05,6.75 9.43,7.63L11.5,13L12.09,11.58C12.22,11.25 12.57,11 13,11H21.78C21.91,10.5 22,10 22,9.5A5.5,5.5 0 0,0 16.5,4C14.64,4 13,4.93 12,6.34C11,4.93 9.36,4 7.5,4V4M3,12.5A1,1 0 0,0 2,13.5A1,1 0 0,0 3,14.5H5.44L11,20C12,20.9 12,20.9 13,20L18.56,14.5H21A1,1 0 0,0 22,13.5A1,1 0 0,0 21,12.5H13.4L12.47,14.8C12.07,15.81 10.92,15.67 10.55,14.83L8.5,9.5L7.54,11.83C7.39,12.21 7.05,12.5 6.6,12.5H3Z" /></svg>

After

Width:  |  Height:  |  Size: 782 B

View File

@ -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

View File

@ -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());

View File

@ -19,6 +19,7 @@
#include "cli/Utils.h"
#include "cli/TextStream.h"
#include "core/PasswordHealth.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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<int>(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;
}

View File

@ -19,7 +19,6 @@
#include "PasswordGenerator.h"
#include "crypto/Random.h"
#include <zxcvbn.h>
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) {

View File

@ -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);

188
src/core/PasswordHealth.cpp Normal file
View File

@ -0,0 +1,188 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QApplication>
#include <QString>
#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<Database> 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<PasswordHealth> 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<PasswordHealth>(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();
}

113
src/core/PasswordHealth.h Normal file
View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_PASSWORDHEALTH_H
#define KEEPASSX_PASSWORDHEALTH_H
#include <QHash>
#include <QSharedPointer>
#include <QStringList>
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<Database>);
// Get the health status of an entry in the database
QSharedPointer<PasswordHealth> evaluate(const Entry* entry);
private:
// Result cache (first=entry UUID)
QHash<QUuid, QSharedPointer<PasswordHealth>> m_cache;
// first = password, second = entries that use it
QHash<QString, QStringList> m_reuse;
};
#endif // KEEPASSX_PASSWORDHEALTH_H

View File

@ -76,7 +76,7 @@ static const QString aboutContributors = R"(
<li>fonic (Entry Table View)</li>
<li>kylemanna (YubiKey)</li>
<li>c4rlo (Offline HIBP Checker)</li>
<li>wolframroesler (HTML Exporter)</li>
<li>wolframroesler (HTML Export, Statistics, Password Health)</li>
<li>mdaniel (OpVault Importer)</li>
<li>keithbennett (KeePassHTTP)</li>
<li>Typz (KeePassHTTP)</li>

View File

@ -457,6 +457,11 @@ void DatabaseTabWidget::changeMasterKey()
currentDatabaseWidget()->switchToMasterKeyChange();
}
void DatabaseTabWidget::changeReports()
{
currentDatabaseWidget()->switchToReports();
}
void DatabaseTabWidget::changeDatabaseSettings()
{
currentDatabaseWidget()->switchToDatabaseSettings();

View File

@ -78,6 +78,7 @@ public slots:
void relockPendingDatabase();
void changeMasterKey();
void changeReports();
void changeDatabaseSettings();
void performGlobalAutoType();

View File

@ -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<Database> 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<Database> 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<Database> 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<Database> 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);

View File

@ -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<EditEntryWidget> m_editEntryWidget;
QPointer<EditGroupWidget> m_editGroupWidget;
QPointer<EditEntryWidget> m_historyEditEntryWidget;
QPointer<ReportsDialog> m_reportsDialog;
QPointer<DatabaseSettingsDialog> m_databaseSettingDialog;
QPointer<DatabaseOpenWidget> m_databaseOpenWidget;
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;

View File

@ -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);

View File

@ -236,6 +236,7 @@
<addaction name="actionDatabaseClose"/>
<addaction name="separator"/>
<addaction name="actionChangeMasterKey"/>
<addaction name="actionReports"/>
<addaction name="actionChangeDatabaseSettings"/>
<addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
@ -532,6 +533,20 @@
<string>Change master &amp;key...</string>
</property>
</action>
<action name="actionReports">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Reports...</string>
</property>
<property name="toolTip">
<string>Statistics, health check, etc.</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="actionChangeDatabaseSettings">
<property name="enabled">
<bool>false</bool>

View File

@ -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
// <https://community.kde.org/KDE_Visual_Design_Group/HIG/Color>
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;
}
}

View File

@ -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();

View File

@ -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

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 <core/Entry.h>
#include <core/Group.h>
class ReportsDialog::ExtraPage
{
public:
ExtraPage(QSharedPointer<IReportsPage> p, QWidget* w)
: page(p)
, widget(w)
{
}
void loadSettings(QSharedPointer<Database> db) const
{
page->loadSettings(widget, db);
}
void saveSettings() const
{
page->saveSettings(widget);
}
private:
QSharedPointer<IReportsPage> 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<Database>& db)
{
m_ui->categoryList->setCurrentCategory(0);
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(db);
}
m_db = db;
}
void ReportsDialog::addPage(QSharedPointer<IReportsPage> 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();
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_REPORTSWIDGET_H
#define KEEPASSX_REPORTSWIDGET_H
#include "config-keepassx.h"
#include "gui/DialogyWidget.h"
#include "gui/entry/EditEntryWidget.h"
#include <QPointer>
#include <QScopedPointer>
#include <QSharedPointer>
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<Database> 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<Database>& db);
void addPage(QSharedPointer<IReportsPage> page);
signals:
void editFinished(bool accepted);
private slots:
void reject();
void entryActivationSignalReceived(const Group*, Entry* entry);
void switchToMainView(bool previousDialogAccepted);
private:
QSharedPointer<Database> m_db;
const QScopedPointer<Ui::ReportsDialog> m_ui;
const QSharedPointer<ReportsPageHealthcheck> m_healthPage;
const QSharedPointer<ReportsPageStatistics> m_statPage;
QPointer<EditEntryWidget> m_editEntryWidget;
class ExtraPage;
QList<ExtraPage> m_extraPages;
};
#endif // KEEPASSX_REPORTSWIDGET_H

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ReportsDialog</class>
<widget class="QWidget" name="ReportsDialog">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
<item>
<widget class="CategoryListWidget" name="categoryList" native="true"/>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>-1</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>CategoryListWidget</class>
<extends>QWidget</extends>
<header>gui/CategoryListWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "ReportsPageHealthcheck.h"
#include "ReportsWidgetHealthcheck.h"
#include "core/FilePath.h"
#include <QApplication>
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<Database> db)
{
const auto settingsWidget = reinterpret_cast<ReportsWidgetHealthcheck*>(widget);
settingsWidget->loadSettings(db);
}
void ReportsPageHealthcheck::saveSettings(QWidget* widget)
{
const auto settingsWidget = reinterpret_cast<ReportsWidgetHealthcheck*>(widget);
settingsWidget->saveSettings();
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
#define KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
#include <QWidget>
#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<Database> db) override;
void saveSettings(QWidget* widget) override;
};
#endif // KEEPASSXC_REPORTSPAGEHEALTHCHECK_H

View File

@ -15,38 +15,36 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#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 <QApplication>
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<Database> db)
void ReportsPageStatistics::loadSettings(QWidget* widget, QSharedPointer<Database> db)
{
DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetStatistics*>(widget);
ReportsWidgetStatistics* settingsWidget = reinterpret_cast<ReportsWidgetStatistics*>(widget);
settingsWidget->loadSettings(db);
}
void DatabaseSettingsPageStatistics::saveSettings(QWidget* widget)
void ReportsPageStatistics::saveSettings(QWidget* widget)
{
DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetStatistics*>(widget);
ReportsWidgetStatistics* settingsWidget = reinterpret_cast<ReportsWidgetStatistics*>(widget);
settingsWidget->saveSettings();
}

View File

@ -15,14 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
#define KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
#ifndef KEEPASSXC_REPORTSPAGESTATISTICS_H
#define KEEPASSXC_REPORTSPAGESTATISTICS_H
#include <QWidget>
#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

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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<Database> db)
{
m_db = std::move(db);
initialize();
}
const QSharedPointer<Database> ReportsWidget::getDatabase() const
{
return m_db;
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_REPORTSWIDGET_H
#define KEEPASSXC_REPORTSWIDGET_H
#include "gui/settings/SettingsWidget.h"
#include <QSharedPointer>
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<Database> db);
const QSharedPointer<Database> getDatabase() const;
signals:
/**
* Can be emitted to indicate size changes and allow parents widgets to adjust properly.
*/
void sizeChanged();
protected:
QSharedPointer<Database> m_db;
};
#endif // KEEPASSXC_REPORTSWIDGET_H

View File

@ -0,0 +1,237 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 <QSharedPointer>
#include <QStandardItemModel>
#include <QVector>
namespace
{
class Health
{
public:
struct Item
{
QPointer<const Group> group;
QPointer<const Entry> entry;
QSharedPointer<PasswordHealth> health;
Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h)
: group(g)
, entry(e)
, health(h)
{
}
bool operator<(const Item& rhs) const
{
return health->score() < rhs.health->score();
}
};
explicit Health(QSharedPointer<Database>);
const QList<QSharedPointer<Item>>& items() const
{
return m_items;
}
private:
QSharedPointer<Database> m_db;
HealthChecker m_checker;
QList<QSharedPointer<Item>> m_items;
};
} // namespace
Health::Health(QSharedPointer<Database> 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<Item>(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<Item> x, QSharedPointer<Item> 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<PasswordHealth> 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<QStandardItem*>();
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<Database> db)
{
m_db = std::move(db);
m_healthCalculated = false;
m_referencesModel->clear();
m_rowToEntry.clear();
auto row = QList<QStandardItem*>();
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> 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*>(entry));
}
}
void ReportsWidgetHealthcheck::saveSettings()
{
// nothing to do - the tab is passive
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
#define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
#include "gui/entry/EntryModel.h"
#include <QHash>
#include <QIcon>
#include <QPair>
#include <QWidget>
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<Database> 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<PasswordHealth>, const Group*, const Entry*);
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
bool m_healthCalculated = false;
QIcon m_errorIcon;
QScopedPointer<QStandardItemModel> m_referencesModel;
QSharedPointer<Database> m_db;
QList<QPair<const Group*, const Entry*>> m_rowToEntry;
};
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ReportsWidgetHealthcheck</class>
<widget class="QWidget" name="ReportsWidgetHealthcheck">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<height>379</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="healthcheckGroupBox">
<property name="title">
<string>Health Check</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="healthcheckTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="tipLabel">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Hover over reason to show additional details. Double-click entries to edit.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -15,15 +15,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#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 <QFileInfo>
#include <QHash>
@ -48,6 +48,7 @@ namespace
// Ctor does all the work
explicit Stats(QSharedPointer<Database> db)
: modified(QFileInfo(db->filePath()).lastModified())
, m_db(db)
{
gatherStats(db->rootGroup()->groupsRecursive(true));
}
@ -92,19 +93,27 @@ namespace
}
private:
QSharedPointer<Database> m_db;
QHash<QString, int> m_passwords;
void gatherStats(const QList<Group*>& 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<QStandardItem*>();
row << new QStandardItem(name);
@ -170,7 +180,7 @@ void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value,
}
};
void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
void ReportsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
{
m_db = std::move(db);
m_statsCalculated = false;
@ -178,7 +188,7 @@ void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer<Database> 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> 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
}

View File

@ -15,8 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
#define KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
#ifndef KEEPASSXC_REPORTSWIDGETSTATISTICS_H
#define KEEPASSXC_REPORTSWIDGETSTATISTICS_H
#include <QIcon>
#include <QWidget>
@ -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<Database> db);
void saveSettings();
@ -46,7 +46,7 @@ private slots:
void calculateStats();
private:
QScopedPointer<Ui::DatabaseSettingsWidgetStatistics> m_ui;
QScopedPointer<Ui::ReportsWidgetStatistics> 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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseSettingsWidgetStatistics</class>
<widget class="QWidget" name="DatabaseSettingsWidgetStatistics">
<class>ReportsWidgetStatistics</class>
<widget class="QWidget" name="ReportsWidgetStatistics">
<property name="geometry">
<rect>
<x>0</x>

View File

@ -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})

View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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());
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_TESTPASSWORDHEALTH_H
#define KEEPASSX_TESTPASSWORDHEALTH_H
#include <QObject>
class TestPasswordHealth : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testNoDb();
};
#endif // KEEPASSX_TESTPASSWORDHEALTH_H

View File

@ -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 ;;