Skip to content

Commit

Permalink
Check passwords against the HIBP online service (keepassxreboot#4438)
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

Authored-by: Wolfram Rösler <[email protected]>
Co-authored-by: Jonathan White <[email protected]>
  • Loading branch information
wolframroesler and droidmonkey authored Mar 29, 2020
1 parent 464e49d commit 83ed9a8
Show file tree
Hide file tree
Showing 18 changed files with 948 additions and 11 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
190 changes: 190 additions & 0 deletions src/core/HibpDownloader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* 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);
return sha1.toHex().toUpper();
}

/*
* 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)
{
// The first 5 characters of the hash are in the URL already,
// the HIBP result contains the remainder
auto pos = hibpResult.indexOf(sha1Hex(password).mid(5));
if (pos < 0) {
return 0;
}

// Skip past the sha1 and ':'
pos += 36;

// Find where the count ends
auto end = hibpResult.indexOf('\n', pos);
if (end < 0) {
end = hibpResult.size();
}

// Extract the count, remove remaining whitespace, and convert to int
return hibpResult.midRef(pos, end - pos).trimmed().toInt();
}
} // namespace

HibpDownloader::HibpDownloader(QObject* parent)
: QObject(parent)
{
}

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

/*
* Add one password to the list list of passwords to check.
*
* Invoke this function once for every password to check,
* then call validate().
*/
void HibpDownloader::add(const QString& password)
{
if (!m_pwdsToTry.contains(password)) {
m_pwdsToTry << password;
}
}

/*
* Start validating the passwords against HIBP.
*/
void HibpDownloader::validate()
{
for (auto password : m_pwdsToTry) {
// 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(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.
auto reply = getNetMgr()->get(request);
connect(reply, &QNetworkReply::finished, this, &HibpDownloader::fetchFinished);
connect(reply, &QIODevice::readyRead, this, &HibpDownloader::fetchReadyRead);
m_replies.insert(reply, {password, {}});
}

m_pwdsToTry.clear();
}

int HibpDownloader::passwordsToValidate() const
{
return m_pwdsToTry.size();
}

int HibpDownloader::passwordsRemaining() const
{
return m_replies.size();
}

/*
* Abort the current online activity (if any).
*/
void HibpDownloader::abort()
{
for (auto reply : m_replies.keys()) {
reply->abort();
reply->deleteLater();
}
m_replies.clear();
}

/*
* Called when new data has been loaded from the HIBP server.
*/
void HibpDownloader::fetchReadyRead()
{
const auto reply = qobject_cast<QNetworkReply*>(sender());
auto entry = m_replies.find(reply);
if (entry != m_replies.end()) {
entry->second += reply->readAll();
}
}

/*
* Called after all data has been loaded from the HIBP server.
*/
void HibpDownloader::fetchFinished()
{
const auto reply = qobject_cast<QNetworkReply*>(sender());
const auto entry = m_replies.find(reply);
if (entry == m_replies.end()) {
return;
}

// Get result status
const auto ok = reply->error() == QNetworkReply::NoError;
const auto err = reply->errorString();

const auto password = entry->first;
const auto hibpReply = entry->second;

reply->deleteLater();
m_replies.remove(reply);

// If there was an error, assume it's permanent and abort
// (don't process the rest of the password list).
if (!ok) {
auto msg = tr("Online password validation failed") + ":\n" + err;
if (!hibpReply.isEmpty()) {
msg += "\n" + hibpReply;
}
abort();
emit fetchFailed(msg);
return;
}

// Current password validated, send the result to the caller
emit hibpResult(password, pwnCount(password, hibpReply));
}
72 changes: 72 additions & 0 deletions src/core/HibpDownloader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 "config-keepassx.h"
#include <QHash>
#include <QObject>
#include <QTimer>

#ifndef WITH_XC_NETWORKING
#error This file requires KeePassXC to be built with network support.
#endif

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(QObject* parent = nullptr);
~HibpDownloader() override;

void add(const QString& password);
void validate();
int passwordsToValidate() const;
int passwordsRemaining() const;

signals:
void hibpResult(const QString& password, int count);
void fetchFailed(const QString& error);

public slots:
void abort();

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

private:
void fetchPassword(const QString& password);

QStringList m_pwdsToTry; // The list of remaining passwords to validate
QHash<QNetworkReply*, QPair<QString, QByteArray>> m_replies;
};

#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: 27 additions & 7 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 @@ -70,9 +74,8 @@ ReportsDialog::ReportsDialog(QWidget* parent)
adjustSize();

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*)));
connect(m_healthPage->m_healthWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
connect(m_hibpPage->m_hibpWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
}

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

void ReportsDialog::entryActivationSignalReceived(const Group* group, Entry* entry)
void ReportsDialog::entryActivationSignalReceived(Entry* entry)
{
m_editEntryWidget->loadEntry(entry, false, false, group->hierarchy().join(" > "), m_db);
m_sender = static_cast<QWidget*>(sender());
m_editEntryWidget->loadEntry(entry, false, false, entry->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;
}
Loading

0 comments on commit 83ed9a8

Please sign in to comment.