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.

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 keepassxreboot#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 Feb 1, 2020
1 parent 71a39c3 commit 11ffde5
Show file tree
Hide file tree
Showing 38 changed files with 1,364 additions and 75 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
188 changes: 188 additions & 0 deletions src/core/PasswordHealth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright (C) 2019 KeePassXC Team <[email protected]>
*
* 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();
}
Loading

0 comments on commit 11ffde5

Please sign in to comment.