Skip to content

Commit

Permalink
Ability to check all passwords against online HIBP database
Browse files Browse the repository at this point in the history
* Fixes keepassxreboot#1083

* Add online HIBP checker dialog to the database reports widget. Permission is requested from the user prior to performing any network operations. 
* The number of times a password has been found in a breach is shown to the user.
* If no passwords are breached then a positive message is presented.

* Source of HIBP icon: https://github.com/simple-icons/simple-icons/blob/develop/icons/haveibeenpwned.svg
  • Loading branch information
wolframroesler committed Mar 21, 2020
1 parent 8e4b0fd commit 4defffb
Show file tree
Hide file tree
Showing 18 changed files with 845 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Added
- Added CLI db-info command [#4231]
- Switch application icons to Material Design [#4066]
- Health Check report [#551]
- HIBP report: Check passwords against the HIBP online service [#1083]

### Changed
- Renamed CLI create command to db-create [#4231]
Expand Down
4 changes: 4 additions & 0 deletions COPYING
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,7 @@ License: MIT
Files: share/icons/application/scalable/apps/freedesktop.svg
Copyright: GPL-2+
Comment: from Freedesktop.org website

Files: share/icons/application/scalable/actions/hibp.svg
Copyright: GPL-2+
Comment: from the Simple Icons repo (https://github.com/simple-icons/simple-icons/)
Binary file modified share/demo.kdbx
Binary file not shown.
1 change: 1 addition & 0 deletions share/icons/application/scalable/actions/hibp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions share/icons/icons.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<file>application/scalable/actions/group-new.svg</file>
<file>application/scalable/actions/health.svg</file>
<file>application/scalable/actions/help-about.svg</file>
<file>application/scalable/actions/hibp.svg</file>
<file>application/scalable/actions/key-enter.svg</file>
<file>application/scalable/actions/keyboard-shortcuts.svg</file>
<file>application/scalable/actions/message-close.svg</file>
Expand Down
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ set(keepassx_SOURCES
gui/reports/ReportsDialog.cpp
gui/reports/ReportsWidgetHealthcheck.cpp
gui/reports/ReportsPageHealthcheck.cpp
gui/reports/ReportsWidgetHibp.cpp
gui/reports/ReportsPageHibp.cpp
gui/reports/ReportsWidgetStatistics.cpp
gui/reports/ReportsPageStatistics.cpp
gui/osutils/OSUtilsBase.cpp
Expand Down Expand Up @@ -287,6 +289,7 @@ endif()

if(WITH_XC_NETWORKING)
list(APPEND keepassx_SOURCES
core/HibpDownloader.cpp
core/IconDownloader.cpp
core/NetworkManager.cpp
gui/UpdateCheckDialog.cpp
Expand Down
156 changes: 156 additions & 0 deletions src/core/HibpDownloader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright (C) 2020 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 "HibpDownloader.h"
#include "core/Config.h"
#include "core/Global.h"
#include "core/NetworkManager.h"

#include <QCryptographicHash>
#include <QUrl>
#include <QtNetwork>

namespace
{

/*
* Return the SHA1 hash of the specified password in upper-case hex.
*
* The result is always exactly 40 characters long.
*/
QString sha1Hex(const QString& password)
{
// Get the binary SHA1
const auto sha1 = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha1);

// Convert to hex
QString ret;
ret.reserve(40);
auto p = sha1.data();
for (int i = 0; i < sha1.size(); ++i) {
const auto u = uint8_t(*p++);
constexpr auto hex = "0123456789ABCDEF";
ret += hex[(u >> 4) & 0xF];
ret += hex[u & 0xF];
}

return ret;
}

/*
* Search a password's hash in the output of the HIBP web service.
*
* Returns the number of times the password is found in breaches, or
* 0 if the password is not in the HIBP result.
*/
int pwnCount(const QString& password, const QString& hibpResult)
{
const auto hash = sha1Hex(password).toStdString();
const auto result = hibpResult.toStdString();
const auto pHash = hash.c_str();
const auto pResult = result.c_str();

// The first 5 characters of the hash are in the URL. Search the
// rest in the HIBP result.
const auto p = strstr(pResult, pHash + 5);
if (p) {

// Found: Return the number after the next colon
const auto colon = strchr(p, ':');
return colon ? atoi(colon + 1) : 1;

} else {

// Not found
return 0;
}
}
} // namespace

HibpDownloader::HibpDownloader(QString password, QObject* parent)
: QObject(parent)
, m_password(password)
{
// Set up timeout handling
m_timeout.setSingleShot(true);
connect(&m_timeout, SIGNAL(timeout()), SLOT(abort()));
const auto sec = config()->get("HibpDownloadTimeout", 10).toInt();
m_timeout.start(sec * 1000);

// The URL we query is https://api.pwnedpasswords.com/range/XXXXX,
// where XXXXX is the first five bytes of the hex representation of
// the password's SHA1.
const auto url = QString("https://api.pwnedpasswords.com/range/") + sha1Hex(m_password).left(5);

// HIBP requires clients to specify a user agent in the request
// (https://haveibeenpwned.com/API/v3#UserAgent); however, in order
// to minimize the amount of information we expose about ourselves,
// we don't add the KeePassXC version number or platform.
auto request = QNetworkRequest(url);
request.setRawHeader("User-Agent", "KeePassXC");

// Finally, submit the request to HIBP.
m_reply = getNetMgr()->get(request);
connect(m_reply, &QNetworkReply::finished, this, &HibpDownloader::fetchFinished);
connect(m_reply, &QIODevice::readyRead, this, &HibpDownloader::fetchReadyRead);
}

HibpDownloader::~HibpDownloader()
{
abort();
}

/*
* Abort the current online activity (if any).
*/
void HibpDownloader::abort()
{
if (m_reply) {
m_reply->abort();
}
}

/*
* Called when new data has been loaded from the HIBP server.
*/
void HibpDownloader::fetchReadyRead()
{
m_bytesReceived += m_reply->readAll();
}

/*
* Called after all data has been loaded from the HIBP server.
*/
void HibpDownloader::fetchFinished()
{
const auto ok = m_reply->error() == QNetworkReply::NoError;
const auto err = m_reply->errorString();

m_reply->deleteLater();
m_reply = nullptr;
m_timeout.stop();

if (ok) {
emit finished(m_password, pwnCount(m_password, m_bytesReceived));
} else {
auto msg = tr("Online password validation failed") + ":\n" + err;
if (!m_bytesReceived.isEmpty()) {
msg += "\n" + m_bytesReceived;
}
emit failed(m_password, msg);
}
}
61 changes: 61 additions & 0 deletions src/core/HibpDownloader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2020 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/>.
*/

#ifndef KEEPASSXC_HIBPDOWNLOADER_H
#define KEEPASSXC_HIBPDOWNLOADER_H

#include <QObject>
#include <QTimer>

class QNetworkReply;

/*
* Check if a password has been hacked by looking it up on the
* "Have I Been Pwned" website (https://haveibeenpwned.com/)
* in the background.
*
* Usage: Pass the password to check to the ctor and process
* the `finished` signal to get the result. Process the
* `failed` signal to handle errors.
*/
class HibpDownloader : public QObject
{
Q_OBJECT

public:
explicit HibpDownloader(QString password, QObject* parent = nullptr);
~HibpDownloader() override;

signals:
void finished(const QString& password, int count);
void failed(const QString& password, const QString& error);

public slots:
void abort();

private slots:
void fetchFinished();
void fetchReadyRead();

private:
const QString m_password;
QNetworkReply* m_reply = nullptr;
QTimer m_timeout;
QByteArray m_bytesReceived;
};

#endif // KEEPASSXC_HIBPDOWNLOADER_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 Export, Statistics, Password Health)</li>
<li>wolframroesler (HTML Export, Statistics, Password Health, HIBP integration)</li>
<li>mdaniel (OpVault Importer)</li>
<li>keithbennett (KeePassHTTP)</li>
<li>Typz (KeePassHTTP)</li>
Expand Down
34 changes: 29 additions & 5 deletions src/gui/reports/ReportsDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
#include "ui_ReportsDialog.h"

#include "ReportsPageHealthcheck.h"
#include "ReportsPageHibp.h"
#include "ReportsPageStatistics.h"
#include "ReportsWidgetHealthcheck.h"
#include "ReportsWidgetHibp.h"

#include "core/Global.h"
#include "touchid/TouchID.h"
Expand Down Expand Up @@ -53,13 +55,15 @@ ReportsDialog::ReportsDialog(QWidget* parent)
: DialogyWidget(parent)
, m_ui(new Ui::ReportsDialog())
, m_healthPage(new ReportsPageHealthcheck())
, m_hibpPage(new ReportsPageHibp())
, m_statPage(new ReportsPageStatistics())
, m_editEntryWidget(new EditEntryWidget(this))
{
m_ui->setupUi(this);

connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
addPage(m_healthPage);
addPage(m_hibpPage);
addPage(m_statPage);

m_ui->stackedWidget->setCurrentIndex(0);
Expand All @@ -71,8 +75,11 @@ ReportsDialog::ReportsDialog(QWidget* parent)

connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
connect(m_healthPage->m_healthWidget,
SIGNAL(entryActivated(const Group*, Entry*)),
SLOT(entryActivationSignalReceived(const Group*, Entry*)));
SIGNAL(entryActivated(QWidget*, const Group*, Entry*)),
SLOT(entryActivationSignalReceived(QWidget*, const Group*, Entry*)));
connect(m_hibpPage->m_hibpWidget,
SIGNAL(entryActivated(QWidget*, const Group*, Entry*)),
SLOT(entryActivationSignalReceived(QWidget*, const Group*, Entry*)));
connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
}

Expand Down Expand Up @@ -113,16 +120,33 @@ void ReportsDialog::reject()
emit editFinished(true);
}

void ReportsDialog::entryActivationSignalReceived(const Group* group, Entry* entry)
void ReportsDialog::entryActivationSignalReceived(QWidget* sender, const Group* group, Entry* entry)
{
m_sender = sender;
m_editEntryWidget->loadEntry(entry, false, false, group->hierarchy().join(" > "), m_db);
m_ui->stackedWidget->setCurrentWidget(m_editEntryWidget);
}

void ReportsDialog::switchToMainView(bool previousDialogAccepted)
{
m_ui->stackedWidget->setCurrentWidget(m_healthPage->m_healthWidget);
// Sanity check
if (!m_sender) {
return;
}

// Return to the previous widget
m_ui->stackedWidget->setCurrentWidget(m_sender);

// If "OK" was clicked, and if we came from the Health Check pane,
// re-compute Health Check
if (previousDialogAccepted) {
m_healthPage->m_healthWidget->calculateHealth();
if (m_sender == m_healthPage->m_healthWidget) {
m_healthPage->m_healthWidget->calculateHealth();
} else if (m_sender == m_hibpPage->m_hibpWidget) {
m_hibpPage->m_hibpWidget->refreshAfterEdit();
}
}

// Don't process the same sender twice
m_sender = nullptr;
}
5 changes: 4 additions & 1 deletion src/gui/reports/ReportsDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Entry;
class Group;
class QTabWidget;
class ReportsPageHealthcheck;
class ReportsPageHibp;
class ReportsPageStatistics;

namespace Ui
Expand Down Expand Up @@ -68,15 +69,17 @@ class ReportsDialog : public DialogyWidget

private slots:
void reject();
void entryActivationSignalReceived(const Group*, Entry* entry);
void entryActivationSignalReceived(QWidget*, const Group*, Entry* entry);
void switchToMainView(bool previousDialogAccepted);

private:
QSharedPointer<Database> m_db;
const QScopedPointer<Ui::ReportsDialog> m_ui;
const QSharedPointer<ReportsPageHealthcheck> m_healthPage;
const QSharedPointer<ReportsPageHibp> m_hibpPage;
const QSharedPointer<ReportsPageStatistics> m_statPage;
QPointer<EditEntryWidget> m_editEntryWidget;
QWidget* m_sender = nullptr;

class ExtraPage;
QList<ExtraPage> m_extraPages;
Expand Down
Loading

0 comments on commit 4defffb

Please sign in to comment.