Skip to content
API Documentation
BastionGuard™
HibpPasswordProvider.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
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.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
22#include "../net/HttpClient.hpp"
23
24#include <openssl/sha.h>
25#include <sstream>
26#include <iomanip>
27#include <map>
28#include <cctype>
29
30static std::string sha1HexUpper(const std::string& input)
31{
32 unsigned char hash[SHA_DIGEST_LENGTH];
33 SHA1(reinterpret_cast<const unsigned char*>(input.data()),
34 input.size(),
35 hash);
36
37 std::ostringstream oss;
38 oss << std::uppercase << std::hex << std::setfill('0');
39
40 for (int i = 0; i < SHA_DIGEST_LENGTH; ++i) {
41 oss << std::setw(2) << static_cast<unsigned int>(hash[i]);
42 }
43
44 return oss.str();
45}
46
47static inline void rtrim_crlf(std::string& s)
48{
49 while (!s.empty() && (s.back() == '\r' || s.back() == '\n'))
50 s.pop_back();
51}
52
54HibpPasswordProvider::checkPassword(const std::string& password)
55{
56 PasswordLeakResult result;
57
58 if (password.empty())
59 return result;
60
61 const std::string hash = sha1HexUpper(password);
62 if (hash.size() != 40)
63 return result;
64
65 const std::string prefix = hash.substr(0, 5);
66 const std::string suffix = hash.substr(5); // 35 chars
67
68 const std::string url =
69 "https://api.pwnedpasswords.com/range/" + prefix;
70
71 std::map<std::string, std::string> headers;
72 headers["User-Agent"] = "BastionGuard/IdentityLeak";
73 headers["Accept"] = "text/plain";
74 headers["Add-Padding"] = "true";
75
76 HttpResponse resp = HttpClient::get(url, headers, 15);
77 if (resp.status != 200)
78 return result;
79
80 std::istringstream stream(resp.body);
81 std::string line;
82
83 while (std::getline(stream, line)) {
84 rtrim_crlf(line);
85
86 const auto pos = line.find(':');
87 if (pos == std::string::npos)
88 continue;
89
90 const std::string_view lineSuffix(line.data(), pos);
91 if (lineSuffix.size() != suffix.size())
92 continue;
93
94 if (lineSuffix == suffix) {
95
96 std::string_view countPart(line.data() + pos + 1,
97 line.size() - (pos + 1));
98
99 unsigned long long count = 0;
100 for (char ch : countPart) {
101 if (!std::isdigit(static_cast<unsigned char>(ch)))
102 break;
103 count = count * 10ULL + static_cast<unsigned long long>(ch - '0');
104 }
105
106 result.compromised = true;
107 result.count = count;
108 break;
109 }
110 }
111
112 return result;
113}
static std::string sha1HexUpper(const std::string &input)
static void rtrim_crlf(std::string &s)
static PasswordLeakResult checkPassword(const std::string &password)
static HttpResponse get(const std::string &url, const std::map< std::string, std::string > &headers={}, long timeoutSeconds=20)
std::string body