Skip to content

Commit f00b337

Browse files
wolframroeslerdroidmonkey
authored andcommitted
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. 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
1 parent b2fd7f6 commit f00b337

38 files changed

Lines changed: 1357 additions & 73 deletions

share/demo.kdbx

13.5 KB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Loading

src/CMakeLists.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ set(keepassx_SOURCES
4848
core/Merger.cpp
4949
core/Metadata.cpp
5050
core/PasswordGenerator.cpp
51+
core/PasswordHealth.cpp
5152
core/PassphraseGenerator.cpp
5253
core/SignalMultiplexer.cpp
5354
core/ScreenLockListener.cpp
@@ -149,8 +150,12 @@ set(keepassx_SOURCES
149150
gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp
150151
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
151152
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
152-
gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
153-
gui/dbsettings/DatabaseSettingsPageStatistics.cpp
153+
gui/reports/ReportsWidget.cpp
154+
gui/reports/ReportsDialog.cpp
155+
gui/reports/ReportsWidgetHealthcheck.cpp
156+
gui/reports/ReportsPageHealthcheck.cpp
157+
gui/reports/ReportsWidgetStatistics.cpp
158+
gui/reports/ReportsPageStatistics.cpp
154159
gui/settings/SettingsWidget.cpp
155160
gui/widgets/ElidedLabel.cpp
156161
gui/widgets/PopupHelpWidget.cpp

src/browser/BrowserSettings.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "BrowserSettings.h"
2121
#include "core/Config.h"
22+
#include "core/PasswordHealth.h"
2223

2324
BrowserSettings* BrowserSettings::m_instance(nullptr);
2425

@@ -541,7 +542,7 @@ QJsonObject BrowserSettings::generatePassword()
541542
m_passwordGenerator.setCharClasses(passwordCharClasses());
542543
m_passwordGenerator.setFlags(passwordGeneratorFlags());
543544
const QString pw = m_passwordGenerator.generatePassword();
544-
password["entropy"] = m_passwordGenerator.estimateEntropy(pw);
545+
password["entropy"] = PasswordHealth(pw).entropy();
545546
password["password"] = pw;
546547
} else {
547548
m_passPhraseGenerator.setWordCount(passPhraseWordCount());

src/cli/Estimate.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "cli/Utils.h"
2020

2121
#include "cli/TextStream.h"
22+
#include "core/PasswordHealth.h"
2223
#include <stdio.h>
2324
#include <stdlib.h>
2425
#include <string.h>
@@ -49,10 +50,9 @@ static void estimate(const char* pwd, bool advanced)
4950
{
5051
TextStream out(Utils::STDOUT, QIODevice::WriteOnly);
5152

52-
double e = 0.0;
5353
int len = static_cast<int>(strlen(pwd));
5454
if (!advanced) {
55-
e = ZxcvbnMatch(pwd, nullptr, nullptr);
55+
const auto e = PasswordHealth(pwd).entropy();
5656
// clang-format off
5757
out << QObject::tr("Length %1").arg(len, 0) << '\t'
5858
<< QObject::tr("Entropy %1").arg(e, 0, 'f', 3) << '\t'
@@ -62,7 +62,7 @@ static void estimate(const char* pwd, bool advanced)
6262
int ChkLen = 0;
6363
ZxcMatch_t *info, *p;
6464
double m = 0.0;
65-
e = ZxcvbnMatch(pwd, nullptr, &info);
65+
const auto e = ZxcvbnMatch(pwd, nullptr, &info);
6666
for (p = info; p; p = p->Next) {
6767
m += p->Entrpy;
6868
}

src/core/PasswordGenerator.cpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
#include "PasswordGenerator.h"
2020

2121
#include "crypto/Random.h"
22-
#include <zxcvbn.h>
2322

2423
const char* PasswordGenerator::DefaultExcludedChars = "";
2524

@@ -31,11 +30,6 @@ PasswordGenerator::PasswordGenerator()
3130
{
3231
}
3332

34-
double PasswordGenerator::estimateEntropy(const QString& password)
35-
{
36-
return ZxcvbnMatch(password.toLatin1(), nullptr, nullptr);
37-
}
38-
3933
void PasswordGenerator::setLength(int length)
4034
{
4135
if (length <= 0) {

src/core/PasswordGenerator.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ class PasswordGenerator
5757
public:
5858
PasswordGenerator();
5959

60-
double estimateEntropy(const QString& password);
6160
void setLength(int length);
6261
void setCharClasses(const CharClasses& classes);
6362
void setFlags(const GeneratorFlags& flags);

src/core/PasswordHealth.cpp

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 2 or (at your option)
7+
* version 3 of the License.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include <QApplication>
19+
#include <QString>
20+
21+
#include "Database.h"
22+
#include "Entry.h"
23+
#include "Group.h"
24+
#include "PasswordHealth.h"
25+
#include "zxcvbn.h"
26+
27+
PasswordHealth::PasswordHealth(double entropy)
28+
: m_entropy(entropy)
29+
, m_score(entropy)
30+
{
31+
switch (quality()) {
32+
case Quality::bad:
33+
case Quality::poor:
34+
m_reason = QApplication::tr("Very weak password");
35+
m_details = QApplication::tr("Password entropy is %1 bit").arg(QString::number(m_entropy, 'f', 2));
36+
break;
37+
38+
case Quality::weak:
39+
m_reason = QApplication::tr("Weak password");
40+
m_details = QApplication::tr("Password entropy is %1 bit").arg(QString::number(m_entropy, 'f', 2));
41+
break;
42+
43+
case Quality::good:
44+
case Quality::excellent:
45+
// Reasons are essentially error messages; if there's nothing
46+
// to complain, leave it empty.
47+
break;
48+
}
49+
}
50+
51+
PasswordHealth::PasswordHealth(QString pwd)
52+
: PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
53+
{
54+
}
55+
56+
PasswordHealth::PasswordHealth(QSharedPointer<Database> db, QString pwd, Cache* cache)
57+
: PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
58+
{
59+
// Whenever the password is re-used, reduce score by
60+
// this many points:
61+
constexpr auto penalty = 15;
62+
63+
if (!db || !db->rootGroup()) {
64+
return;
65+
}
66+
67+
// Set up the cache if not yet done (and use our own cache if
68+
// the caller didn't give us one)
69+
Cache mycache;
70+
if (!cache) {
71+
cache = &mycache;
72+
}
73+
if (cache->isEmpty()) {
74+
const auto groups = db->rootGroup()->groupsRecursive(true);
75+
for (const auto* group : groups) {
76+
if (!group->isRecycled()) {
77+
for (const auto* entry : group->entries()) {
78+
if (!entry->isRecycled()) {
79+
(*cache)[entry->password()]
80+
<< QApplication::tr("Used in %1/%2").arg(group->hierarchy().join('/'), entry->title());
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
// If the password is in the database more than once,
88+
// reduce the score accordingly
89+
const auto& used = (*cache)[pwd];
90+
const auto count = used.size();
91+
if (count > 1) {
92+
m_score -= penalty * (count - 1);
93+
addTo(m_reason, QApplication::tr("Password is used %1 times").arg(QString::number(count)));
94+
addTo(m_details, used.join('\n'));
95+
96+
// Don't allow re-used passwords to be considered "good"
97+
// no matter how great their entropy is.
98+
if (m_score > 64) {
99+
m_score = 64;
100+
}
101+
}
102+
}
103+
104+
PasswordHealth::PasswordHealth(QSharedPointer<Database> db, const Entry& entry, Cache* cache)
105+
: PasswordHealth(db, entry.password(), cache)
106+
{
107+
// If the password has already expired, reduce score to 0.
108+
// Else, if the password is going to expire in the next
109+
// 30 days, reduce score by 2 points per day.
110+
if (entry.isExpired()) {
111+
m_score = 0;
112+
addTo(m_reason, QApplication::tr("Password has expired"));
113+
addTo(m_details,
114+
QApplication::tr("Password expiry was %1")
115+
.arg(entry.timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate)));
116+
} else if (entry.timeInfo().expires()) {
117+
const auto days = QDateTime::currentDateTime().daysTo(entry.timeInfo().expiryTime());
118+
if (days <= 30) {
119+
// First bring the score down into the "weak" range
120+
// so that the entry appears in Health Check. Then
121+
// reduce the score by 2 points for every day that
122+
// we get closer to expiry. days<=0 has already
123+
// been handled above ("isExpired()").
124+
if (m_score > 60) {
125+
m_score = 60;
126+
}
127+
m_score -= (30 - days) * 2;
128+
addTo(m_reason,
129+
days <= 2 ? QApplication::tr("Password is about to expire")
130+
: days <= 10 ? QApplication::tr("Password expires in %1 days").arg(days)
131+
: QApplication::tr("Password will expire soon"));
132+
addTo(m_details,
133+
QApplication::tr("Password expires on %1")
134+
.arg(entry.timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate)));
135+
}
136+
}
137+
}
138+
139+
void PasswordHealth::addTo(QString& to, QString newText)
140+
{
141+
if (!to.isEmpty()) {
142+
to += '\n';
143+
}
144+
to += newText;
145+
}
146+
147+
PasswordHealth::Quality PasswordHealth::quality() const
148+
{
149+
const auto s = score();
150+
return s <= 0 ? Quality::bad
151+
: s < 40 ? Quality::poor : s < 65 ? Quality::weak : s < 100 ? Quality::good : Quality::excellent;
152+
}
153+
154+
QColor PasswordHealth::color() const
155+
{
156+
switch (quality()) {
157+
case Quality::bad:
158+
return QColor("red");
159+
160+
case Quality::poor:
161+
return QColor("orange");
162+
163+
case Quality::weak:
164+
return QColor("yellow");
165+
166+
case Quality::good:
167+
return QColor("green");
168+
169+
case Quality::excellent:
170+
return QColor("green");
171+
}
172+
173+
// Unreachable. Using "return" here instead of "default"
174+
// above because we want a compiler warning when someone
175+
// adds new enum values.
176+
return QColor();
177+
}

src/core/PasswordHealth.h

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 2 or (at your option)
7+
* version 3 of the License.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#ifndef KEEPASSX_PASSWORDHEALTH_H
19+
#define KEEPASSX_PASSWORDHEALTH_H
20+
21+
#include <QHash>
22+
#include <QSharedPointer>
23+
#include <QStringList>
24+
25+
class Database;
26+
class Entry;
27+
class QString;
28+
29+
class PasswordHealth
30+
{
31+
public:
32+
// first = the password
33+
// second = paths of where this password is used
34+
using Cache = QHash<QString, QStringList>;
35+
36+
/*
37+
* Constructors.
38+
* Callers may pass a pointer to a Cache object in order to
39+
* speed up repeated calls on the same database. (Don't re-use
40+
* a cache across databases.)
41+
*/
42+
PasswordHealth() = default;
43+
explicit PasswordHealth(double entropy);
44+
explicit PasswordHealth(QString pwd);
45+
PasswordHealth(QSharedPointer<Database> db, QString pwd, Cache* cache = nullptr);
46+
PasswordHealth(QSharedPointer<Database> db, const Entry& entry, Cache* cache = nullptr);
47+
48+
/*
49+
* The password score is defined to be the greater the better
50+
* (more secure) the password is. It doesn't have a dimension,
51+
* there are no defined maximum or minimum values, and score
52+
* values may change with different versions of the software.
53+
*/
54+
int score() const
55+
{
56+
return m_score;
57+
}
58+
59+
/*
60+
* The password quality assessment (based on the score).
61+
*/
62+
enum class Quality
63+
{
64+
bad,
65+
poor,
66+
weak,
67+
good,
68+
excellent
69+
};
70+
Quality quality() const;
71+
72+
/*
73+
* A color that matches the quality.
74+
*/
75+
QColor color() const;
76+
77+
/*
78+
* A text description for the password's quality assessment
79+
* (translated into the application language), and additional
80+
* information. Empty if nothing is wrong with the password.
81+
* May contain more than line, separated by '\n'.
82+
*/
83+
QString reason() const
84+
{
85+
return m_reason;
86+
}
87+
QString details() const
88+
{
89+
return m_details;
90+
}
91+
92+
/*
93+
* The password entropy, in bits.
94+
*/
95+
double entropy() const
96+
{
97+
return m_entropy;
98+
}
99+
100+
private:
101+
double m_entropy = 0.0;
102+
int m_score = 0;
103+
QString m_reason, m_details;
104+
void addTo(QString& to, QString newText);
105+
};
106+
107+
#endif // KEEPASSX_PASSWORDHEALTH_H

src/gui/AboutDialog.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ static const QString aboutContributors = R"(
7676
<li>fonic (Entry Table View)</li>
7777
<li>kylemanna (YubiKey)</li>
7878
<li>c4rlo (Offline HIBP Checker)</li>
79-
<li>wolframroesler (HTML Exporter)</li>
79+
<li>wolframroesler (HTML Export, Statistics, Password Health)</li>
8080
<li>mdaniel (OpVault Importer)</li>
8181
<li>keithbennett (KeePassHTTP)</li>
8282
<li>Typz (KeePassHTTP)</li>

0 commit comments

Comments
 (0)