Skip to content

Commit

Permalink
Further improved Password Health classes
Browse files Browse the repository at this point in the history
  • Loading branch information
droidmonkey committed Jan 28, 2020
1 parent 999f57c commit e9d37bf
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 212 deletions.
175 changes: 85 additions & 90 deletions src/core/PasswordHealth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,24 @@
#include "PasswordHealth.h"
#include "zxcvbn.h"

namespace {
/*
* Helper function to add a string to another as a new line
*/
void addTo(QString& to, QString newText)
{
if (!to.isEmpty()) {
to += '\n';
}
to += newText;
}
}

PasswordHealth::PasswordHealth(double entropy)
: m_entropy(entropy)
, m_score(entropy)
: m_score(entropy)
, m_entropy(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));
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_reason = QApplication::tr("Weak password");
m_details = QApplication::tr("Password entropy is %1 bit").arg(QString::number(m_entropy, 'f', 2));
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;

case Quality::good:
case Quality::excellent:
// Reasons are essentially error messages; if there's nothing
// to complain, leave it empty.
default:
// No reason or details for good and excellent passwords
break;
}
}
Expand All @@ -66,55 +51,61 @@ PasswordHealth::PasswordHealth(QString pwd)
{
}

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

QColor PasswordHealth::color() const
void PasswordHealth::adjustScore(int amount)
{
switch (quality()) {
case Quality::bad:
return QColor("red");
m_score += amount;
}

case Quality::poor:
return QColor("orange");
QString PasswordHealth::scoreReason() const
{
return m_scoreReasons.join("\n");
}

void PasswordHealth::addScoreReason(QString reason)
{
m_scoreReasons << reason;
}

case Quality::weak:
return QColor("yellow");
QString PasswordHealth::scoreDetails() const
{
return m_scoreDetails.join("\n");
}

case Quality::good:
return QColor("green");
void PasswordHealth::addScoreDetails(QString details)
{
m_scoreDetails.append(details);
}

case Quality::excellent:
return QColor("green");
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;
}

// Unreachable. Using "return" here instead of "default"
// above because we want a compiler warning when someone
// adds new enum values.
return QColor();
return Quality::Excellent;
}

/**
* Ctor of the health checker class.
*
* 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* group : db->rootGroup()->groupsRecursive(true)) {
if (!group->isRecycled()) {
for (const auto* entry : group->entries()) {
if (!entry->isRecycled()) {
m_reuse[entry->password()]
<< QApplication::tr("Used in %1/%2").arg(group->hierarchy().join('/'), entry->title());
}
}
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());
}
}
}
Expand All @@ -125,69 +116,73 @@ HealthChecker::HealthChecker(QSharedPointer<Database> db)
* Returns the health of the password in `entry`, considering
* password entropy, re-use, expiration, etc.
*/
const PasswordHealth& HealthChecker::operator()(const Entry& entry)
QSharedPointer<PasswordHealth> HealthChecker::evaluate(const Entry* entry)
{
// Return from cache if we saw it before
const auto p = m_cache.find(entry.uuid());
if (p!=m_cache.end()) {
return *p;
if (!entry) {
return {};
}

// Got this entry for the first time. Store its health here:
auto& health = m_cache[entry.uuid()];
// 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();
health = PasswordHealth(pwd);
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.m_score -= penalty * (count - 1);
addTo(health.m_reason, QApplication::tr("Password is used %1 times").arg(QString::number(count)));
addTo(health.m_details, used.join('\n'));
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.m_score > 64) {
health.m_score = 64;
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.m_score = 0;
addTo(health.m_reason, QApplication::tr("Password has expired"));
addTo(health.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 (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.m_score > 60) {
health.m_score = 60;
if (health->score() > 60) {
health->setScore(60);
}
health.m_score -= (30 - days) * 2;
addTo(health.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(health.m_details,
QApplication::tr("Password expires on %1")
.arg(entry.timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate)));
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 health;
return m_cache.insert(entry->uuid(), health).value();
}
69 changes: 31 additions & 38 deletions src/core/PasswordHealth.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

class Database;
class Entry;
class QString;

/**
* Health status of a single password.
Expand All @@ -33,11 +32,7 @@ class QString;
*/
class PasswordHealth
{
friend class HealthChecker;

public:
// Ctors
PasswordHealth() = default;
explicit PasswordHealth(double entropy);
explicit PasswordHealth(QString pwd);

Expand All @@ -52,69 +47,67 @@ class PasswordHealth
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;
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 reason() const
{
return m_reason;
}
QString details() const
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
{
return m_details;
}
Bad,
Poor,
Weak,
Good,
Excellent
};
Quality quality() const;

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

private:
double m_entropy = 0.0;
int m_score = 0;
QString m_reason, m_details;
double m_entropy = 0.0;
QStringList m_scoreReasons;
QStringList m_scoreDetails;
};

/**
* Password health check for all entries of a database.
*
* @see PasswordHealth
*/
class HealthChecker {
class HealthChecker
{
public:
// Ctor
explicit HealthChecker(QSharedPointer<Database>);

// Get the health status of an entry
const PasswordHealth& operator()(const Entry&);
// Get the health status of an entry in the database
QSharedPointer<PasswordHealth> evaluate(const Entry* entry);

private:
QHash<QUuid, PasswordHealth> m_cache; // Result cache (first=entry UUID)
QHash<QString, QStringList> m_reuse; // first=password, second=where it is used
// 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
Loading

0 comments on commit e9d37bf

Please sign in to comment.