Check passwords against the HIBP online service (#4438)

* Fixes #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 <wolfram@roesler-ac.de>
Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
Wolfram Rösler 2020-03-29 15:39:06 +02:00 committed by GitHub
parent 464e49d329
commit 83ed9a8d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 949 additions and 12 deletions

View File

@ -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]

View File

@ -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 not shown.

View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>haveibeenpwned icon</title><path d="M3.03 2.37a2.09 2.09 0 0 0-.333.02.905.905 0 0 0-.244.067.527.527 0 0 0-.166.088.5.5 0 0 0-.133.144L.652 4.943a4.084 4.084 0 0 0-.308.532 2.184 2.184 0 0 0-.2.496c-.051.17-.089.351-.11.543A5.53 5.53 0 0 0 0 7.143c0 .199.022.356.066.474a.48.48 0 0 0 .188.266.745.745 0 0 0 .32.123c.133.023.288.031.465.031v.002c.184 0 .342-.01.475-.033a.857.857 0 0 0 .332-.123.56.56 0 0 0 .187-.266c.044-.118.067-.275.067-.474V5.729L3.547 2.71a.418.418 0 0 0 .045-.154.177.177 0 0 0-.067-.112.342.342 0 0 0-.177-.054 2.17 2.17 0 0 0-.319-.022zm4.347 5.61a2.03 2.03 0 0 0-.559.065.647.647 0 0 0-.343.205.813.813 0 0 0-.182.387 3.436 3.436 0 0 0-.043.6c0 .228.014.422.043.58a.883.883 0 0 0 .182.386.75.75 0 0 0 .343.205c.144.036.33.053.559.053a2.2 2.2 0 0 0 .547-.053.75.75 0 0 0 .344-.205.75.75 0 0 0 .183-.387c.036-.157.053-.35.053-.58a2.77 2.77 0 0 0-.053-.6.702.702 0 0 0-.183-.386.647.647 0 0 0-.344-.205 1.937 1.937 0 0 0-.547-.065zm4.05 3.69a.337.337 0 0 0-.308.172c-.068.114-.103.316-.103.605 0 .29.035.496.103.617.069.115.172.17.309.17h4.779c.13 0 .228-.055.297-.17.068-.114.101-.32.101-.617 0-.145-.01-.268-.033-.367a.637.637 0 0 0-.08-.24.25.25 0 0 0-.127-.137.335.335 0 0 0-.158-.033zm7.395 0a.337.337 0 0 0-.308.172c-.069.114-.104.316-.104.605 0 .29.035.496.104.617.068.115.171.17.308.17H23.6c.13 0 .228-.055.296-.17.07-.114.104-.32.104-.617 0-.145-.012-.268-.035-.367a.637.637 0 0 0-.08-.24.247.247 0 0 0-.125-.137.343.343 0 0 0-.16-.033zm-11.32 4.29c-.184 0-.342.012-.475.034a.857.857 0 0 0-.332.123.56.56 0 0 0-.187.266 1.383 1.383 0 0 0-.067.474v1.414L4.994 21.29a.418.418 0 0 0-.045.154c.007.044.03.082.067.112.036.03.096.046.177.054.089.013.195.022.32.022.133 0 .242-.006.33-.022a.905.905 0 0 0 .245-.066.527.527 0 0 0 .166-.088.5.5 0 0 0 .133-.144l1.504-2.254a4.12 4.12 0 0 0 .308-.532c.089-.162.153-.326.197-.496.052-.17.09-.351.112-.543.022-.191.033-.4.033-.629a1.38 1.38 0 0 0-.066-.474.477.477 0 0 0-.188-.266.745.745 0 0 0-.32-.123 2.795 2.795 0 0 0-.465-.031z"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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>

View File

@ -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
@ -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

190
src/core/HibpDownloader.cpp Normal file
View File

@ -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
src/core/HibpDownloader.h Normal file
View File

@ -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

View File

@ -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>

View File

@ -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"
@ -53,6 +55,7 @@ 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))
{
@ -60,6 +63,7 @@ ReportsDialog::ReportsDialog(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
addPage(m_healthPage);
addPage(m_hibpPage);
addPage(m_statPage);
m_ui->stackedWidget->setCurrentIndex(0);
@ -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)));
}
@ -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);
if (previousDialogAccepted) {
m_healthPage->m_healthWidget->calculateHealth();
// 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) {
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;
}

View File

@ -31,6 +31,7 @@ class Entry;
class Group;
class QTabWidget;
class ReportsPageHealthcheck;
class ReportsPageHibp;
class ReportsPageStatistics;
namespace Ui
@ -68,15 +69,17 @@ signals:
private slots:
void reject();
void entryActivationSignalReceived(const Group*, Entry* entry);
void entryActivationSignalReceived(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;

View File

@ -0,0 +1,55 @@
/*
* 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 "ReportsPageHibp.h"
#include "ReportsWidgetHibp.h"
#include "core/Resources.h"
#include <QApplication>
ReportsPageHibp::ReportsPageHibp()
: m_hibpWidget(new ReportsWidgetHibp())
{
}
QString ReportsPageHibp::name()
{
return QApplication::tr("HIBP");
}
QIcon ReportsPageHibp::icon()
{
return resources()->icon("hibp");
}
QWidget* ReportsPageHibp::createWidget()
{
return m_hibpWidget;
}
void ReportsPageHibp::loadSettings(QWidget* widget, QSharedPointer<Database> db)
{
const auto settingsWidget = reinterpret_cast<ReportsWidgetHibp*>(widget);
settingsWidget->loadSettings(db);
}
void ReportsPageHibp::saveSettings(QWidget* widget)
{
const auto settingsWidget = reinterpret_cast<ReportsWidgetHibp*>(widget);
settingsWidget->saveSettings();
}

View File

@ -0,0 +1,41 @@
/*
* 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_REPORTSPAGEHIBP_H
#define KEEPASSXC_REPORTSPAGEHIBP_H
#include <QWidget>
#include "ReportsDialog.h"
class ReportsWidgetHibp;
class ReportsPageHibp : public IReportsPage
{
public:
ReportsWidgetHibp* m_hibpWidget;
ReportsPageHibp();
QString name() override;
QIcon icon() override;
QWidget* createWidget() override;
void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
void saveSettings(QWidget* widget) override;
};
#endif // KEEPASSXC_REPORTSPAGEHIBP_H

View File

@ -227,7 +227,7 @@ void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
const auto group = row.first;
const auto entry = row.second;
if (group && entry) {
emit entryActivated(group, const_cast<Entry*>(entry));
emit entryActivated(const_cast<Entry*>(entry));
}
}

View File

@ -49,7 +49,7 @@ protected:
void showEvent(QShowEvent* event) override;
signals:
void entryActivated(const Group* group, Entry* entry);
void entryActivated(Entry*);
public slots:
void calculateHealth();

View File

@ -0,0 +1,269 @@
/*
* 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 "ReportsWidgetHibp.h"
#include "ui_ReportsWidgetHibp.h"
#include "config-keepassx.h"
#include "core/Database.h"
#include "core/Group.h"
#include "gui/MessageBox.h"
#include <QStandardItemModel>
ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ReportsWidgetHibp())
{
m_ui->setupUi(this);
m_referencesModel.reset(new QStandardItemModel());
m_ui->hibpTableView->setModel(m_referencesModel.data());
m_ui->hibpTableView->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
#ifdef WITH_XC_NETWORKING
connect(&m_downloader, SIGNAL(hibpResult(QString, int)), SLOT(addHibpResult(QString, int)));
connect(&m_downloader, SIGNAL(fetchFailed(QString)), SLOT(fetchFailed(QString)));
connect(m_ui->validationButton, &QPushButton::pressed, [this] { startValidation(); });
#endif
}
ReportsWidgetHibp::~ReportsWidgetHibp()
{
}
void ReportsWidgetHibp::loadSettings(QSharedPointer<Database> db)
{
// Re-initialize
m_db = std::move(db);
m_referencesModel->clear();
m_pwndPasswords.clear();
m_error.clear();
m_rowToEntry.clear();
m_editedEntry = nullptr;
#ifdef WITH_XC_NETWORKING
m_ui->stackedWidget->setCurrentIndex(0);
m_ui->validationButton->setEnabled(true);
m_ui->progressBar->hide();
#else
// Compiled without networking, can't do anything
m_ui->stackedWidget->setCurrentIndex(2);
#endif
}
/*
* Fill the table will all entries that have passwords that we've
* found to have been pwned.
*/
void ReportsWidgetHibp::makeHibpTable()
{
// Reset the table
m_referencesModel->clear();
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Password exposed…"));
m_rowToEntry.clear();
// Search database for passwords that we've found so far
QList<QPair<const Entry*, int>> items;
for (const auto* entry : m_db->rootGroup()->entriesRecursive()) {
if (!entry->isRecycled()) {
const auto found = m_pwndPasswords.find(entry->password());
if (found != m_pwndPasswords.end()) {
items.append({entry, found.value()});
}
}
}
// Sort decending by the number the password has been exposed
qSort(items.begin(), items.end(), [](QPair<const Entry*, int>& lhs, QPair<const Entry*, int>& rhs) {
return lhs.second > rhs.second;
});
// Build the table
for (const auto& item : items) {
const auto entry = item.first;
const auto group = entry->group();
const auto count = item.second;
auto row = QList<QStandardItem*>();
row << new QStandardItem(entry->iconPixmap(), entry->title())
<< new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"))
<< new QStandardItem(countToText(count));
m_referencesModel->appendRow(row);
row[2]->setForeground(QBrush(QColor("red")));
// Store entry pointer per table row (used in double click handler)
m_rowToEntry.append(entry);
}
// If there was an error, append the error message to the table
if (!m_error.isEmpty()) {
auto row = QList<QStandardItem*>();
row << new QStandardItem(m_error);
m_referencesModel->appendRow(row);
row[0]->setForeground(QBrush(QColor("red")));
}
// If we're done and everything is good, display a motivational message
#ifdef WITH_XC_NETWORKING
if (m_downloader.passwordsRemaining() == 0 && m_pwndPasswords.isEmpty() && m_error.isEmpty()) {
m_referencesModel->clear();
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, no exposed passwords!"));
}
#endif
m_ui->hibpTableView->resizeRowsToContents();
m_ui->stackedWidget->setCurrentIndex(1);
}
/*
* Invoked when the downloader has finished checking one password.
*/
void ReportsWidgetHibp::addHibpResult(const QString& password, int count)
{
// Add the password to the list of our findings if it has been pwned
if (count > 0) {
m_pwndPasswords[password] = count;
}
#ifdef WITH_XC_NETWORKING
// Update the progress bar
int remaining = m_downloader.passwordsRemaining();
if (remaining > 0) {
m_ui->progressBar->setValue(m_ui->progressBar->maximum() - remaining);
} else {
// Finished, remove the progress bar and build the table
m_ui->progressBar->hide();
makeHibpTable();
}
#endif
}
/*
* Invoked when a query to the HIBP server fails.
*
* Displays the table with the current findings.
*/
void ReportsWidgetHibp::fetchFailed(const QString& error)
{
m_error = error;
m_ui->progressBar->hide();
makeHibpTable();
}
/*
* Add passwords to the downloader and start the actual online validation.
*/
void ReportsWidgetHibp::startValidation()
{
#ifdef WITH_XC_NETWORKING
// Collect all passwords in the database (unless recycled, and
// unless empty) and submit them to the downloader.
for (const auto* entry : m_db->rootGroup()->entriesRecursive()) {
if (!entry->isRecycled() && !entry->password().isEmpty()) {
m_downloader.add(entry->password());
}
}
// Store the number of passwords we need to check for the progress bar
m_ui->progressBar->show();
m_ui->progressBar->setMaximum(m_downloader.passwordsToValidate());
m_ui->validationButton->setEnabled(false);
m_downloader.validate();
#endif
}
/*
* Convert the number of times a password has been pwned into
* a display text for the third table column.
*/
QString ReportsWidgetHibp::countToText(int count)
{
if (count == 1) {
return tr("once");
} else if (count <= 10) {
return tr("up to 10 times");
} else if (count <= 100) {
return tr("up to 100 times");
} else if (count <= 1000) {
return tr("up to 1000 times");
} else if (count <= 10000) {
return tr("up to 10,000 times");
} else if (count <= 100000) {
return tr("up to 100,000 times");
} else if (count <= 1000000) {
return tr("up to a million times");
}
return tr("millions of times");
}
/*
* Double-click handler
*/
void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index)
{
if (!index.isValid()) {
return;
}
// Find which database entry was double-clicked
const auto entry = m_rowToEntry[index.row()];
if (entry) {
// Found it, invoke entry editor
m_editedEntry = entry;
m_editedPassword = entry->password();
emit entryActivated(const_cast<Entry*>(entry));
}
}
/*
* Invoked after "OK" was clicked in the entry editor.
* Re-validates the edited entry's new password.
*/
void ReportsWidgetHibp::refreshAfterEdit()
{
// Sanity check
if (!m_editedEntry) {
return;
}
// No need to re-validate if there was no change
if (m_editedEntry->password() == m_editedPassword) {
return;
}
// Remove the previous password from the list of findings
m_pwndPasswords.remove(m_editedPassword);
// Validate the new password against HIBP
#ifdef WITH_XC_NETWORKING
m_downloader.add(m_editedEntry->password());
m_downloader.validate();
#endif
m_editedEntry = nullptr;
}
void ReportsWidgetHibp::saveSettings()
{
// nothing to do - the tab is passive
}

View File

@ -0,0 +1,82 @@
/*
* 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_REPORTSWIDGETHIBP_H
#define KEEPASSXC_REPORTSWIDGETHIBP_H
#include "config-keepassx.h"
#include "gui/entry/EntryModel.h"
#include <QMap>
#include <QPair>
#include <QPointer>
#include <QWidget>
#ifdef WITH_XC_NETWORKING
#include "core/HibpDownloader.h"
#endif
class Database;
class Entry;
class Group;
class QStandardItemModel;
namespace Ui
{
class ReportsWidgetHibp;
}
class ReportsWidgetHibp : public QWidget
{
Q_OBJECT
public:
explicit ReportsWidgetHibp(QWidget* parent = nullptr);
~ReportsWidgetHibp();
void loadSettings(QSharedPointer<Database> db);
void saveSettings();
void refreshAfterEdit();
signals:
void entryActivated(Entry*);
public slots:
void emitEntryActivated(const QModelIndex&);
void addHibpResult(const QString&, int);
void fetchFailed(const QString& error);
private:
void makeHibpTable();
void startValidation();
QString countToText(int count);
QScopedPointer<Ui::ReportsWidgetHibp> m_ui;
QScopedPointer<QStandardItemModel> m_referencesModel;
QSharedPointer<Database> m_db;
QMap<QString, int> m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
QString m_error; // Error message if download failed, else empty
QList<const Entry*> m_rowToEntry; // List index is table row
QPointer<const Entry> m_editedEntry; // The entry we're currently editing
QString m_editedPassword; // The old password of the entry we're editing
#ifdef WITH_XC_NETWORKING
HibpDownloader m_downloader; // This performs the actual HIBP online query
#endif
};
#endif // KEEPASSXC_REPORTSWIDGETHIBP_H

View File

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ReportsWidgetHibp</class>
<widget class="QWidget" name="ReportsWidgetHibp">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>545</width>
<height>379</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Have I Been Pwned?</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="confirmation">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>15</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>450</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>CAUTION: This report requires sending information to the Have I Been Pwned online service (https://haveibeenpwned.com). If you proceed, your database passwords will be cryptographically hashed and the first five characters of those hashes will be sent securely to this service. Your database remains secure and cannot be reconstituted from this information. However, the number of passwords you send and your IP address will be exposed to this service.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="validationButton">
<property name="maximumSize">
<size>
<width>275</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Perform Online Analysis</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="resultsTable">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="hibpTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="noNetwork">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="networkNoticeLabel">
<property name="maximumSize">
<size>
<width>450</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>