mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-22 15:39:57 -05:00
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:
parent
464e49d329
commit
83ed9a8d4e
@ -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]
|
||||
|
4
COPYING
4
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/)
|
||||
|
BIN
share/demo.kdbx
BIN
share/demo.kdbx
Binary file not shown.
1
share/icons/application/scalable/actions/hibp.svg
Normal file
1
share/icons/application/scalable/actions/hibp.svg
Normal 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 |
@ -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>
|
||||
|
@ -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
190
src/core/HibpDownloader.cpp
Normal 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
72
src/core/HibpDownloader.h
Normal 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
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
55
src/gui/reports/ReportsPageHibp.cpp
Normal file
55
src/gui/reports/ReportsPageHibp.cpp
Normal 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();
|
||||
}
|
41
src/gui/reports/ReportsPageHibp.h
Normal file
41
src/gui/reports/ReportsPageHibp.h
Normal 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
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
269
src/gui/reports/ReportsWidgetHibp.cpp
Normal file
269
src/gui/reports/ReportsWidgetHibp.cpp
Normal 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
|
||||
}
|
82
src/gui/reports/ReportsWidgetHibp.h
Normal file
82
src/gui/reports/ReportsWidgetHibp.h
Normal 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
|
193
src/gui/reports/ReportsWidgetHibp.ui
Normal file
193
src/gui/reports/ReportsWidgetHibp.ui
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user