2
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "HibpDownloader.h"
19
#include "core/NetworkManager.h"
21
#include <QCryptographicHash>
22
#include <QNetworkReply>
27
* Return the SHA1 hash of the specified password in upper-case hex.
29
* The result is always exactly 40 characters long.
31
QString sha1Hex(const QString& password)
33
// Get the binary SHA1
34
const auto sha1 = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha1);
35
return sha1.toHex().toUpper();
39
* Search a password's hash in the output of the HIBP web service.
41
* Returns the number of times the password is found in breaches, or
42
* 0 if the password is not in the HIBP result.
44
int pwnCount(const QString& password, const QString& hibpResult)
46
// The first 5 characters of the hash are in the URL already,
47
// the HIBP result contains the remainder
48
auto pos = hibpResult.indexOf(sha1Hex(password).mid(5));
53
// Skip past the sha1 and ':'
56
// Find where the count ends
57
auto end = hibpResult.indexOf('\n', pos);
59
end = hibpResult.size();
62
// Extract the count, remove remaining whitespace, and convert to int
63
return hibpResult.midRef(pos, end - pos).trimmed().toInt();
67
HibpDownloader::HibpDownloader(QObject* parent)
72
HibpDownloader::~HibpDownloader()
78
* Add one password to the list list of passwords to check.
80
* Invoke this function once for every password to check,
81
* then call validate().
83
void HibpDownloader::add(const QString& password)
85
if (!m_pwdsToTry.contains(password)) {
86
m_pwdsToTry << password;
91
* Start validating the passwords against HIBP.
93
void HibpDownloader::validate()
95
for (auto password : m_pwdsToTry) {
96
// The URL we query is https://api.pwnedpasswords.com/range/XXXXX,
97
// where XXXXX is the first five bytes of the hex representation of
98
// the password's SHA1.
99
const auto url = QString("https://api.pwnedpasswords.com/range/") + sha1Hex(password).left(5);
101
// HIBP requires clients to specify a user agent in the request
102
// (https://haveibeenpwned.com/API/v3#UserAgent); however, in order
103
// to minimize the amount of information we expose about ourselves,
104
// we don't add the KeePassXC version number or platform.
105
auto request = QNetworkRequest(url);
106
request.setRawHeader("User-Agent", "KeePassXC");
108
// Finally, submit the request to HIBP.
109
auto reply = getNetMgr()->get(request);
110
connect(reply, &QNetworkReply::finished, this, &HibpDownloader::fetchFinished);
111
connect(reply, &QIODevice::readyRead, this, &HibpDownloader::fetchReadyRead);
112
m_replies.insert(reply, {password, {}});
118
int HibpDownloader::passwordsToValidate() const
120
return m_pwdsToTry.size();
123
int HibpDownloader::passwordsRemaining() const
125
return m_replies.size();
129
* Abort the current online activity (if any).
131
void HibpDownloader::abort()
133
for (auto reply : m_replies.keys()) {
135
reply->deleteLater();
141
* Called when new data has been loaded from the HIBP server.
143
void HibpDownloader::fetchReadyRead()
145
const auto reply = qobject_cast<QNetworkReply*>(sender());
146
auto entry = m_replies.find(reply);
147
if (entry != m_replies.end()) {
148
entry->second += reply->readAll();
153
* Called after all data has been loaded from the HIBP server.
155
void HibpDownloader::fetchFinished()
157
const auto reply = qobject_cast<QNetworkReply*>(sender());
158
const auto entry = m_replies.find(reply);
159
if (entry == m_replies.end()) {
164
const auto ok = reply->error() == QNetworkReply::NoError;
165
const auto err = reply->errorString();
167
const auto password = entry->first;
168
const auto hibpReply = entry->second;
170
reply->deleteLater();
171
m_replies.remove(reply);
173
// If there was an error, assume it's permanent and abort
174
// (don't process the rest of the password list).
176
auto msg = tr("Online password validation failed") + ":\n" + err;
177
if (!hibpReply.isEmpty()) {
178
msg += "\n" + hibpReply;
181
emit fetchFailed(msg);
185
// Current password validated, send the result to the caller
186
emit hibpResult(password, pwnCount(password, hibpReply));