Skip to content

Commit

Permalink
Implement Password Health Report
Browse files Browse the repository at this point in the history
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.

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
  • Loading branch information
wolframroesler authored and droidmonkey committed Jan 20, 2020
1 parent b2fd7f6 commit f00b337
Show file tree
Hide file tree
Showing 38 changed files with 1,357 additions and 73 deletions.
Binary file modified share/demo.kdbx
Binary file not shown.
1 change: 1 addition & 0 deletions share/icons/application/scalable/actions/health.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/browser/BrowserSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "BrowserSettings.h"
#include "core/Config.h"
#include "core/PasswordHealth.h"

BrowserSettings* BrowserSettings::m_instance(nullptr);

Expand Down Expand Up @@ -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());
Expand Down
6 changes: 3 additions & 3 deletions src/cli/Estimate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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'
Expand All @@ -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;
}
Expand Down
6 changes: 0 additions & 6 deletions src/core/PasswordGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
#include "PasswordGenerator.h"

#include "crypto/Random.h"
#include <zxcvbn.h>

const char* PasswordGenerator::DefaultExcludedChars = "";

Expand All @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion src/core/PasswordGenerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ class PasswordGenerator
public:
PasswordGenerator();

double estimateEntropy(const QString& password);
void setLength(int length);
void setCharClasses(const CharClasses& classes);
void setFlags(const GeneratorFlags& flags);
Expand Down
177 changes: 177 additions & 0 deletions src/core/PasswordHealth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* 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_entropy(entropy)
, m_score(entropy)
{
switch (quality()) {
case Quality::bad:
case Quality::poor:
m_reason = QApplication::tr("Very weak password");
m_details = QApplication::tr("Password entropy is %1 bit").arg(QString::number(m_entropy, 'f', 2));
break;

case Quality::weak:
m_reason = QApplication::tr("Weak password");
m_details = QApplication::tr("Password entropy is %1 bit").arg(QString::number(m_entropy, 'f', 2));
break;

case Quality::good:
case Quality::excellent:
// Reasons are essentially error messages; if there's nothing
// to complain, leave it empty.
break;
}
}

PasswordHealth::PasswordHealth(QString pwd)
: PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
{
}

PasswordHealth::PasswordHealth(QSharedPointer<Database> db, QString pwd, Cache* cache)
: PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
{
// Whenever the password is re-used, reduce score by
// this many points:
constexpr auto penalty = 15;

if (!db || !db->rootGroup()) {
return;
}

// Set up the cache if not yet done (and use our own cache if
// the caller didn't give us one)
Cache mycache;
if (!cache) {
cache = &mycache;
}
if (cache->isEmpty()) {
const auto groups = db->rootGroup()->groupsRecursive(true);
for (const auto* group : groups) {
if (!group->isRecycled()) {
for (const auto* entry : group->entries()) {
if (!entry->isRecycled()) {
(*cache)[entry->password()]
<< QApplication::tr("Used in %1/%2").arg(group->hierarchy().join('/'), entry->title());
}
}
}
}
}

// If the password is in the database more than once,
// reduce the score accordingly
const auto& used = (*cache)[pwd];
const auto count = used.size();
if (count > 1) {
m_score -= penalty * (count - 1);
addTo(m_reason, QApplication::tr("Password is used %1 times").arg(QString::number(count)));
addTo(m_details, used.join('\n'));

// Don't allow re-used passwords to be considered "good"
// no matter how great their entropy is.
if (m_score > 64) {
m_score = 64;
}
}
}

PasswordHealth::PasswordHealth(QSharedPointer<Database> db, const Entry& entry, Cache* cache)
: PasswordHealth(db, entry.password(), cache)
{
// If the password has already expired, reduce score to 0.
// Else, if the password is going to expire in the next
// 30 days, reduce score by 2 points per day.
if (entry.isExpired()) {
m_score = 0;
addTo(m_reason, QApplication::tr("Password has expired"));
addTo(m_details,
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 (m_score > 60) {
m_score = 60;
}
m_score -= (30 - days) * 2;
addTo(m_reason,
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"));
addTo(m_details,
QApplication::tr("Password expires on %1")
.arg(entry.timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate)));
}
}
}

void PasswordHealth::addTo(QString& to, QString newText)
{
if (!to.isEmpty()) {
to += '\n';
}
to += newText;
}

PasswordHealth::Quality PasswordHealth::quality() const
{
const auto s = score();
return s <= 0 ? Quality::bad
: s < 40 ? Quality::poor : s < 65 ? Quality::weak : s < 100 ? Quality::good : Quality::excellent;
}

QColor PasswordHealth::color() const
{
switch (quality()) {
case Quality::bad:
return QColor("red");

case Quality::poor:
return QColor("orange");

case Quality::weak:
return QColor("yellow");

case Quality::good:
return QColor("green");

case Quality::excellent:
return QColor("green");
}

// Unreachable. Using "return" here instead of "default"
// above because we want a compiler warning when someone
// adds new enum values.
return QColor();
}
107 changes: 107 additions & 0 deletions src/core/PasswordHealth.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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;
class QString;

class PasswordHealth
{
public:
// first = the password
// second = paths of where this password is used
using Cache = QHash<QString, QStringList>;

/*
* Constructors.
* Callers may pass a pointer to a Cache object in order to
* speed up repeated calls on the same database. (Don't re-use
* a cache across databases.)
*/
PasswordHealth() = default;
explicit PasswordHealth(double entropy);
explicit PasswordHealth(QString pwd);
PasswordHealth(QSharedPointer<Database> db, QString pwd, Cache* cache = nullptr);
PasswordHealth(QSharedPointer<Database> db, const Entry& entry, Cache* cache = nullptr);

/*
* 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;
}

/*
* The password quality assessment (based on the score).
*/
enum class Quality
{
bad,
poor,
weak,
good,
excellent
};
Quality quality() const;

/*
* A color that matches the quality.
*/
QColor color() const;

/*
* 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 reason() const
{
return m_reason;
}
QString details() const
{
return m_details;
}

/*
* The password entropy, in bits.
*/
double entropy() const
{
return m_entropy;
}

private:
double m_entropy = 0.0;
int m_score = 0;
QString m_reason, m_details;
void addTo(QString& to, QString newText);
};

#endif // KEEPASSX_PASSWORDHEALTH_H
2 changes: 1 addition & 1 deletion src/gui/AboutDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading

0 comments on commit f00b337

Please sign in to comment.