-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
b2fd7f6
commit f00b337
Showing
38 changed files
with
1,357 additions
and
73 deletions.
There are no files selected for viewing
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.