diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bdcb5bf0..615fb34f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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]
diff --git a/COPYING b/COPYING
index f06c6b225..d5508a925 100644
--- a/COPYING
+++ b/COPYING
@@ -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/)
diff --git a/share/demo.kdbx b/share/demo.kdbx
index 1f3727104..1f2cb1472 100644
Binary files a/share/demo.kdbx and b/share/demo.kdbx differ
diff --git a/share/icons/application/scalable/actions/hibp.svg b/share/icons/application/scalable/actions/hibp.svg
new file mode 100644
index 000000000..c0af23f39
--- /dev/null
+++ b/share/icons/application/scalable/actions/hibp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc
index 61e2b618c..d61251848 100644
--- a/share/icons/icons.qrc
+++ b/share/icons/icons.qrc
@@ -37,6 +37,7 @@
application/scalable/actions/group-new.svgapplication/scalable/actions/health.svgapplication/scalable/actions/help-about.svg
+ application/scalable/actions/hibp.svgapplication/scalable/actions/key-enter.svgapplication/scalable/actions/keyboard-shortcuts.svgapplication/scalable/actions/message-close.svg
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e9e9a7334..24f44bdc5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -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
diff --git a/src/core/HibpDownloader.cpp b/src/core/HibpDownloader.cpp
new file mode 100644
index 000000000..53189af91
--- /dev/null
+++ b/src/core/HibpDownloader.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team
+ *
+ * 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 .
+ */
+
+#include "HibpDownloader.h"
+#include "core/Config.h"
+#include "core/Global.h"
+#include "core/NetworkManager.h"
+
+#include
+#include
+#include
+
+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(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(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));
+}
diff --git a/src/core/HibpDownloader.h b/src/core/HibpDownloader.h
new file mode 100644
index 000000000..04c109bae
--- /dev/null
+++ b/src/core/HibpDownloader.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team
+ *
+ * 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 .
+ */
+
+#ifndef KEEPASSXC_HIBPDOWNLOADER_H
+#define KEEPASSXC_HIBPDOWNLOADER_H
+
+#include "config-keepassx.h"
+#include
+#include
+#include
+
+#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> m_replies;
+};
+
+#endif // KEEPASSXC_HIBPDOWNLOADER_H
diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp
index f9b85ac63..518a85891 100644
--- a/src/gui/AboutDialog.cpp
+++ b/src/gui/AboutDialog.cpp
@@ -76,7 +76,7 @@ static const QString aboutContributors = R"(
diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp
index 22ebab41a..2fdc24389 100644
--- a/src/gui/reports/ReportsDialog.cpp
+++ b/src/gui/reports/ReportsDialog.cpp
@@ -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(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;
}
diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h
index 7a53623c3..a82d7545e 100644
--- a/src/gui/reports/ReportsDialog.h
+++ b/src/gui/reports/ReportsDialog.h
@@ -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 m_db;
const QScopedPointer m_ui;
const QSharedPointer m_healthPage;
+ const QSharedPointer m_hibpPage;
const QSharedPointer m_statPage;
QPointer m_editEntryWidget;
+ QWidget* m_sender = nullptr;
class ExtraPage;
QList m_extraPages;
diff --git a/src/gui/reports/ReportsPageHibp.cpp b/src/gui/reports/ReportsPageHibp.cpp
new file mode 100644
index 000000000..8f5640a20
--- /dev/null
+++ b/src/gui/reports/ReportsPageHibp.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team
+ *
+ * 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 .
+ */
+
+#include "ReportsPageHibp.h"
+
+#include "ReportsWidgetHibp.h"
+#include "core/Resources.h"
+
+#include
+
+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 db)
+{
+ const auto settingsWidget = reinterpret_cast(widget);
+ settingsWidget->loadSettings(db);
+}
+
+void ReportsPageHibp::saveSettings(QWidget* widget)
+{
+ const auto settingsWidget = reinterpret_cast(widget);
+ settingsWidget->saveSettings();
+}
diff --git a/src/gui/reports/ReportsPageHibp.h b/src/gui/reports/ReportsPageHibp.h
new file mode 100644
index 000000000..9d74347c9
--- /dev/null
+++ b/src/gui/reports/ReportsPageHibp.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team
+ *
+ * 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 .
+ */
+
+#ifndef KEEPASSXC_REPORTSPAGEHIBP_H
+#define KEEPASSXC_REPORTSPAGEHIBP_H
+
+#include
+
+#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 db) override;
+ void saveSettings(QWidget* widget) override;
+};
+
+#endif // KEEPASSXC_REPORTSPAGEHIBP_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
index 49370d5f8..1212a5970 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.cpp
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -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));
+ emit entryActivated(const_cast(entry));
}
}
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h
index bf0cf531e..86931c9db 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.h
+++ b/src/gui/reports/ReportsWidgetHealthcheck.h
@@ -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();
diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp
new file mode 100644
index 000000000..1c625d131
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHibp.cpp
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team
+ *
+ * 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 .
+ */
+
+#include "ReportsWidgetHibp.h"
+#include "ui_ReportsWidgetHibp.h"
+
+#include "config-keepassx.h"
+#include "core/Database.h"
+#include "core/Group.h"
+#include "gui/MessageBox.h"
+
+#include
+
+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 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> 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& lhs, QPair& 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();
+ 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();
+ 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));
+ }
+}
+
+/*
+ * 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
+}
diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h
new file mode 100644
index 000000000..b49ef082e
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHibp.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team
+ *
+ * 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 .
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGETHIBP_H
+#define KEEPASSXC_REPORTSWIDGETHIBP_H
+
+#include "config-keepassx.h"
+#include "gui/entry/EntryModel.h"
+
+#include
+#include
+#include
+#include
+
+#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 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 m_ui;
+ QScopedPointer m_referencesModel;
+ QSharedPointer m_db;
+
+ QMap m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
+ QString m_error; // Error message if download failed, else empty
+ QList m_rowToEntry; // List index is table row
+ QPointer 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
diff --git a/src/gui/reports/ReportsWidgetHibp.ui b/src/gui/reports/ReportsWidgetHibp.ui
new file mode 100644
index 000000000..e3eccfd34
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHibp.ui
@@ -0,0 +1,193 @@
+
+
+ ReportsWidgetHibp
+
+
+
+ 0
+ 0
+ 545
+ 379
+
+
+
+
+
+
+ Have I Been Pwned?
+
+
+
+
+
+ 0
+
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+ 450
+ 16777215
+
+
+
+ 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.
+
+
+ true
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+ 275
+ 16777215
+
+
+
+ Perform Online Analysis
+
+
+ true
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ true
+
+
+ Qt::ElideMiddle
+
+
+ false
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ 450
+ 16777215
+
+
+
+ This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.
+
+
+ true
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+