Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check passwords against the HIBP online service #4438

Merged
merged 16 commits into from
Mar 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <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 "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 <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 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