]> git.ipfire.org Git - thirdparty/pdns.git/blame - pdns/credentials.cc
Meson: Separate test files from common files
[thirdparty/pdns.git] / pdns / credentials.cc
CommitLineData
cfe95ada
RG
1/*
2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
8 *
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22#include "config.h"
23
8a6030b6 24#include <cmath>
cfe95ada
RG
25#include <stdexcept>
26
27#ifdef HAVE_LIBSODIUM
28#include <sodium.h>
29#endif
30
2f32819a 31#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
32#include <openssl/evp.h>
33#include <openssl/kdf.h>
8535f666 34#include <openssl/opensslv.h>
8a6030b6
RG
35#include <openssl/rand.h>
36#endif
37
061bc5f0
RG
38#include <fcntl.h>
39#include <sys/stat.h>
40#include <unistd.h>
41
8a6030b6 42#include "base64.hh"
3dc4644f 43#include "dns_random.hh"
cfe95ada 44#include "credentials.hh"
b2504b29 45#include "misc.hh"
cfe95ada 46
2f32819a 47#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
48static size_t const pwhash_max_size = 128U; /* maximum size of the output */
49static size_t const pwhash_output_size = 32U; /* size of the hashed output (before base64 encoding) */
50static unsigned int const pwhash_salt_size = 16U; /* size of the salt (before base64 encoding */
31ec1c9f 51static uint64_t const pwhash_max_work_factor = 32768U; /* max N for interactive login purposes */
8a6030b6
RG
52
53/* PHC string format, storing N as log2(N) as done by passlib.
54 for now we only support one algo but we might have to change that later */
55static std::string const pwhash_prefix = "$scrypt$";
56static size_t const pwhash_prefix_size = pwhash_prefix.size();
57#endif
58
59uint64_t const CredentialsHolder::s_defaultWorkFactor{1024U}; /* N */
60uint64_t const CredentialsHolder::s_defaultParallelFactor{1U}; /* p */
61uint64_t const CredentialsHolder::s_defaultBlockSize{8U}; /* r */
62
31ec1c9f
RG
63SensitiveData::SensitiveData(std::string&& data) :
64 d_data(std::move(data))
8a6030b6 65{
c13de8e8 66 data.clear();
8a6030b6
RG
67#ifdef HAVE_LIBSODIUM
68 sodium_mlock(d_data.data(), d_data.size());
69#endif
70}
71
71633799 72SensitiveData& SensitiveData::operator=(SensitiveData&& rhs) noexcept
0bc984f9
RG
73{
74 d_data = std::move(rhs.d_data);
c13de8e8 75 rhs.clear();
0bc984f9
RG
76 return *this;
77}
78
8a6030b6
RG
79SensitiveData::SensitiveData(size_t bytes)
80{
81 d_data.resize(bytes);
82#ifdef HAVE_LIBSODIUM
83 sodium_mlock(d_data.data(), d_data.size());
84#endif
85}
86
87SensitiveData::~SensitiveData()
88{
89 clear();
90}
91
92void SensitiveData::clear()
93{
94#ifdef HAVE_LIBSODIUM
95 sodium_munlock(d_data.data(), d_data.size());
96#endif
97 d_data.clear();
98}
99
aa87b287 100static std::string hashPasswordInternal([[maybe_unused]] const std::string& password, [[maybe_unused]] const std::string& salt, [[maybe_unused]] uint64_t workFactor, [[maybe_unused]] uint64_t parallelFactor, [[maybe_unused]] uint64_t blockSize)
8a6030b6 101{
2f32819a 102#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
31ec1c9f 103 auto pctx = std::unique_ptr<EVP_PKEY_CTX, void (*)(EVP_PKEY_CTX*)>(EVP_PKEY_CTX_new_id(EVP_PKEY_SCRYPT, nullptr), EVP_PKEY_CTX_free);
8a6030b6
RG
104 if (!pctx) {
105 throw std::runtime_error("Error getting a scrypt context to hash the supplied password");
106 }
107
108 if (EVP_PKEY_derive_init(pctx.get()) <= 0) {
109 throw std::runtime_error("Error intializing the scrypt context to hash the supplied password");
110 }
111
8535f666
RG
112 // OpenSSL 3.0 changed the string arg to const unsigned char*, other versions use const char *
113#if OPENSSL_VERSION_MAJOR >= 3
114 auto passwordData = reinterpret_cast<const char*>(password.data());
115#else
116 auto passwordData = reinterpret_cast<const unsigned char*>(password.data());
117#endif
118 if (EVP_PKEY_CTX_set1_pbe_pass(pctx.get(), passwordData, password.size()) <= 0) {
8a6030b6 119 throw std::runtime_error("Error adding the password to the scrypt context to hash the supplied password");
31ec1c9f 120 }
8a6030b6 121
810cb195 122 if (EVP_PKEY_CTX_set1_scrypt_salt(pctx.get(), reinterpret_cast<const unsigned char*>(salt.data()), salt.size()) <= 0) {
8a6030b6
RG
123 throw std::runtime_error("Error adding the salt to the scrypt context to hash the supplied password");
124 }
125
126 if (EVP_PKEY_CTX_set_scrypt_N(pctx.get(), workFactor) <= 0) {
127 throw std::runtime_error("Error setting the work factor to the scrypt context to hash the supplied password");
128 }
129
130 if (EVP_PKEY_CTX_set_scrypt_r(pctx.get(), blockSize) <= 0) {
131 throw std::runtime_error("Error setting the block size to the scrypt context to hash the supplied password");
132 }
133
134 if (EVP_PKEY_CTX_set_scrypt_p(pctx.get(), parallelFactor) <= 0) {
135 throw std::runtime_error("Error setting the parallel factor to the scrypt context to hash the supplied password");
136 }
137
138 std::string out;
139 out.resize(pwhash_output_size);
140 size_t outlen = out.size();
141
142 if (EVP_PKEY_derive(pctx.get(), reinterpret_cast<unsigned char*>(out.data()), &outlen) <= 0 || outlen != pwhash_output_size) {
143 throw std::runtime_error("Error deriving the output from the scrypt context to hash the supplied password");
144 }
145
146 return out;
b59e6712
RG
147#else
148 throw std::runtime_error("Hashing support is not available");
8a6030b6 149#endif
b59e6712 150}
8a6030b6
RG
151
152static std::string generateRandomSalt()
153{
2f32819a 154#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
155 /* generate a random salt */
156 std::string salt;
157 salt.resize(pwhash_salt_size);
158
159 if (RAND_bytes(reinterpret_cast<unsigned char*>(salt.data()), salt.size()) != 1) {
160 throw std::runtime_error("Error while generating a salt to hash the supplied password");
161 }
162
163 return salt;
164#else
165 throw std::runtime_error("Generating a salted password requires scrypt support in OpenSSL, and it is not available");
166#endif
167}
168
aa87b287 169std::string hashPassword([[maybe_unused]] const std::string& password, [[maybe_unused]] uint64_t workFactor, [[maybe_unused]] uint64_t parallelFactor, [[maybe_unused]] uint64_t blockSize)
cfe95ada 170{
2f32819a
RG
171#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
172 if (workFactor == 0) {
173 throw std::runtime_error("Invalid work factor of " + std::to_string(workFactor) + " passed to hashPassword()");
174 }
175
cfe95ada 176 std::string result;
8a6030b6 177 result.reserve(pwhash_max_size);
cfe95ada 178
8a6030b6
RG
179 result.append(pwhash_prefix);
180 result.append("ln=");
4ec3ff03 181 result.append(std::to_string(static_cast<uint64_t>(std::log2(workFactor))));
8a6030b6 182 result.append(",p=");
4ec3ff03 183 result.append(std::to_string(parallelFactor));
8a6030b6 184 result.append(",r=");
4ec3ff03 185 result.append(std::to_string(blockSize));
8a6030b6
RG
186 result.append("$");
187 auto salt = generateRandomSalt();
188 result.append(Base64Encode(salt));
189 result.append("$");
190
4ec3ff03 191 auto out = hashPasswordInternal(password, salt, workFactor, parallelFactor, blockSize);
8a6030b6
RG
192
193 result.append(Base64Encode(out));
cfe95ada
RG
194
195 return result;
196#else
8a6030b6
RG
197 throw std::runtime_error("Hashing a password requires scrypt support in OpenSSL, and it is not available");
198#endif
199}
200
aa87b287 201std::string hashPassword([[maybe_unused]] const std::string& password)
4ec3ff03 202{
2f32819a 203#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
4ec3ff03
RG
204 return hashPassword(password, CredentialsHolder::s_defaultWorkFactor, CredentialsHolder::s_defaultParallelFactor, CredentialsHolder::s_defaultBlockSize);
205#else
206 throw std::runtime_error("Hashing a password requires scrypt support in OpenSSL, and it is not available");
207#endif
208}
209
aa87b287 210bool verifyPassword([[maybe_unused]] const std::string& binaryHash, [[maybe_unused]] const std::string& salt, [[maybe_unused]] uint64_t workFactor, [[maybe_unused]] uint64_t parallelFactor, [[maybe_unused]] uint64_t blockSize, [[maybe_unused]] const std::string& binaryPassword)
8a6030b6 211{
2f32819a 212#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
213 auto expected = hashPasswordInternal(binaryPassword, salt, workFactor, parallelFactor, blockSize);
214 return constantTimeStringEquals(expected, binaryHash);
215#else
216 throw std::runtime_error("Hashing a password requires scrypt support in OpenSSL, and it is not available");
217#endif
218}
219
220/* parse a hashed password in PHC string format */
aa87b287 221static void parseHashed([[maybe_unused]] const std::string& hash, [[maybe_unused]] std::string& salt, [[maybe_unused]] std::string& hashedPassword, [[maybe_unused]] uint64_t& workFactor, [[maybe_unused]] uint64_t& parallelFactor, [[maybe_unused]] uint64_t& blockSize)
8a6030b6 222{
2f32819a 223#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
224 auto parametersEnd = hash.find('$', pwhash_prefix.size());
225 if (parametersEnd == std::string::npos || parametersEnd == hash.size()) {
8a6030b6
RG
226 throw std::runtime_error("Invalid hashed password format, no parameters");
227 }
228
229 auto parametersStr = hash.substr(pwhash_prefix.size(), parametersEnd);
230 std::vector<std::string> parameters;
231 parameters.reserve(3);
232 stringtok(parameters, parametersStr, ",");
233 if (parameters.size() != 3) {
234 throw std::runtime_error("Invalid hashed password format, expecting 3 parameters, got " + std::to_string(parameters.size()));
235 }
236
237 if (!boost::starts_with(parameters.at(0), "ln=")) {
238 throw std::runtime_error("Invalid hashed password format, ln= parameter not found");
239 }
240
241 if (!boost::starts_with(parameters.at(1), "p=")) {
242 throw std::runtime_error("Invalid hashed password format, p= parameter not found");
243 }
244
245 if (!boost::starts_with(parameters.at(2), "r=")) {
246 throw std::runtime_error("Invalid hashed password format, r= parameter not found");
247 }
248
249 auto saltPos = parametersEnd + 1;
250 auto saltEnd = hash.find('$', saltPos);
251 if (saltEnd == std::string::npos || saltEnd == hash.size()) {
252 throw std::runtime_error("Invalid hashed password format");
253 }
254
255 try {
a0383aad 256 workFactor = pdns::checked_stoi<uint64_t>(parameters.at(0).substr(3));
0ef92c6c 257 workFactor = static_cast<uint64_t>(1) << workFactor;
8a6030b6
RG
258 if (workFactor > pwhash_max_work_factor) {
259 throw std::runtime_error("Invalid work factor of " + std::to_string(workFactor) + " in hashed password string, maximum is " + std::to_string(pwhash_max_work_factor));
260 }
261
a0383aad
FM
262 parallelFactor = pdns::checked_stoi<uint64_t>(parameters.at(1).substr(2));
263 blockSize = pdns::checked_stoi<uint64_t>(parameters.at(2).substr(2));
8a6030b6
RG
264
265 auto b64Salt = hash.substr(saltPos, saltEnd - saltPos);
266 salt.reserve(pwhash_salt_size);
267 B64Decode(b64Salt, salt);
268
269 if (salt.size() != pwhash_salt_size) {
270 throw std::runtime_error("Invalid salt in hashed password string");
271 }
272
273 hashedPassword.reserve(pwhash_output_size);
274 B64Decode(hash.substr(saltEnd + 1), hashedPassword);
275
276 if (hashedPassword.size() != pwhash_output_size) {
277 throw std::runtime_error("Invalid hash in hashed password string");
278 }
279 }
280 catch (const std::exception& e) {
281 throw std::runtime_error("Invalid hashed password format, unable to parse parameters");
282 }
cfe95ada
RG
283#endif
284}
285
aa87b287 286bool verifyPassword(const std::string& hash, [[maybe_unused]] const std::string& password)
cfe95ada 287{
8a6030b6
RG
288 if (!isPasswordHashed(hash)) {
289 return false;
cfe95ada
RG
290 }
291
2f32819a 292#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
293 std::string salt;
294 std::string hashedPassword;
295 uint64_t workFactor = 0;
296 uint64_t parallelFactor = 0;
297 uint64_t blockSize = 0;
298 parseHashed(hash, salt, hashedPassword, workFactor, parallelFactor, blockSize);
299
300 auto expected = hashPasswordInternal(password, salt, workFactor, parallelFactor, blockSize);
301
302 return constantTimeStringEquals(expected, hashedPassword);
cfe95ada 303#else
8a6030b6 304 throw std::runtime_error("Verifying a hashed password requires scrypt support in OpenSSL, and it is not available");
cfe95ada
RG
305#endif
306}
307
aa87b287 308bool isPasswordHashed([[maybe_unused]] const std::string& password)
cfe95ada 309{
2f32819a 310#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
8a6030b6
RG
311 if (password.size() < pwhash_prefix_size || password.size() > pwhash_max_size) {
312 return false;
313 }
314
315 if (!boost::starts_with(password, pwhash_prefix)) {
cfe95ada
RG
316 return false;
317 }
318
8a6030b6
RG
319 auto parametersEnd = password.find('$', pwhash_prefix.size());
320 if (parametersEnd == std::string::npos || parametersEnd == password.size()) {
321 return false;
322 }
cfe95ada 323
4ec3ff03
RG
324 size_t parametersSize = parametersEnd - pwhash_prefix.size();
325 /* ln=X,p=Y,r=Z */
326 if (parametersSize < 12) {
327 return false;
328 }
329
8a6030b6
RG
330 auto saltEnd = password.find('$', parametersEnd + 1);
331 if (saltEnd == std::string::npos || saltEnd == password.size()) {
cfe95ada
RG
332 return false;
333 }
8a6030b6
RG
334
335 /* the salt is base64 encoded so it has to be larger than that */
336 if ((saltEnd - parametersEnd - 1) < pwhash_salt_size) {
337 return false;
338 }
339
340 /* the hash base64 encoded so it has to be larger than that */
31ec1c9f 341 if ((password.size() - saltEnd - 1) < pwhash_output_size) {
8a6030b6
RG
342 return false;
343 }
344
cfe95ada
RG
345 return true;
346#else
347 return false;
348#endif
349}
350
351/* if the password is in cleartext and hashing is available,
352 the hashed form will be kept in memory */
31ec1c9f
RG
353CredentialsHolder::CredentialsHolder(std::string&& password, bool hashPlaintext) :
354 d_credentials(std::move(password))
cfe95ada 355{
cfe95ada 356 if (isHashingAvailable()) {
8a6030b6 357 if (!isPasswordHashed(d_credentials.getString())) {
64c4f83c 358 if (hashPlaintext) {
8a6030b6
RG
359 d_salt = generateRandomSalt();
360 d_workFactor = s_defaultWorkFactor;
361 d_parallelFactor = s_defaultParallelFactor;
362 d_blockSize = s_defaultBlockSize;
363 d_credentials = SensitiveData(hashPasswordInternal(d_credentials.getString(), d_salt, d_workFactor, d_parallelFactor, d_blockSize));
64c4f83c
RG
364 d_isHashed = true;
365 }
cfe95ada
RG
366 }
367 else {
32e8669a 368 d_wasHashed = true;
64c4f83c 369 d_isHashed = true;
8a6030b6
RG
370 std::string hashedPassword;
371 parseHashed(d_credentials.getString(), d_salt, hashedPassword, d_workFactor, d_parallelFactor, d_blockSize);
372 d_credentials = SensitiveData(std::move(hashedPassword));
cfe95ada
RG
373 }
374 }
64c4f83c
RG
375
376 if (!d_isHashed) {
3dc4644f 377 d_fallbackHashPerturb = dns_random_uint32();
8a6030b6 378 d_fallbackHash = burtle(reinterpret_cast<const unsigned char*>(d_credentials.getString().data()), d_credentials.getString().size(), d_fallbackHashPerturb);
cfe95ada 379 }
cfe95ada
RG
380}
381
382CredentialsHolder::~CredentialsHolder()
383{
b2504b29
RG
384 d_fallbackHashPerturb = 0;
385 d_fallbackHash = 0;
cfe95ada
RG
386}
387
388bool CredentialsHolder::matches(const std::string& password) const
389{
64c4f83c 390 if (d_isHashed) {
8a6030b6 391 return verifyPassword(d_credentials.getString(), d_salt, d_workFactor, d_parallelFactor, d_blockSize, password);
cfe95ada
RG
392 }
393 else {
b2504b29
RG
394 uint32_t fallback = burtle(reinterpret_cast<const unsigned char*>(password.data()), password.size(), d_fallbackHashPerturb);
395 if (fallback != d_fallbackHash) {
396 return false;
397 }
398
8a6030b6 399 return constantTimeStringEquals(password, d_credentials.getString());
cfe95ada
RG
400 }
401}
402
403bool CredentialsHolder::isHashingAvailable()
404{
2f32819a 405#if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
cfe95ada
RG
406 return true;
407#else
408 return false;
409#endif
410}
061bc5f0 411
dc6aa7f5 412#include <csignal>
061bc5f0
RG
413#include <termios.h>
414
8a6030b6 415SensitiveData CredentialsHolder::readFromTerminal()
061bc5f0
RG
416{
417 struct termios term;
418 struct termios oterm;
061bc5f0
RG
419 bool restoreTermSettings = false;
420 int termAction = TCSAFLUSH;
421#ifdef TCSASOFT
9437be08 422 termAction |= TCSASOFT;
061bc5f0
RG
423#endif
424
425 FDWrapper input(open("/dev/tty", O_RDONLY));
426 if (int(input) != -1) {
427 if (tcgetattr(input, &oterm) == 0) {
428 memcpy(&term, &oterm, sizeof(term));
429 term.c_lflag &= ~(ECHO | ECHONL);
430 tcsetattr(input, termAction, &term);
431 restoreTermSettings = true;
432 }
433 }
434 else {
435 input = FDWrapper(dup(STDIN_FILENO));
d78e47e3 436 restoreTermSettings = false;
061bc5f0 437 }
d78e47e3 438
061bc5f0
RG
439 FDWrapper output(open("/dev/tty", O_WRONLY));
440 if (int(output) == -1) {
441 output = FDWrapper(dup(STDERR_FILENO));
442 }
443
444 struct std::map<int, struct sigaction> signals;
445 struct sigaction sa;
446 sigemptyset(&sa.sa_mask);
447 sa.sa_flags = 0;
d73de874 448 sa.sa_handler = [](int /* s */) {};
061bc5f0
RG
449 sigaction(SIGALRM, &sa, &signals[SIGALRM]);
450 sigaction(SIGHUP, &sa, &signals[SIGHUP]);
451 sigaction(SIGINT, &sa, &signals[SIGINT]);
452 sigaction(SIGPIPE, &sa, &signals[SIGPIPE]);
453 sigaction(SIGQUIT, &sa, &signals[SIGQUIT]);
454 sigaction(SIGTERM, &sa, &signals[SIGTERM]);
455 sigaction(SIGTSTP, &sa, &signals[SIGTSTP]);
456 sigaction(SIGTTIN, &sa, &signals[SIGTTIN]);
457 sigaction(SIGTTOU, &sa, &signals[SIGTTOU]);
458
459 std::string buffer;
460 /* let's allocate a huge buffer now to prevent reallocation,
461 which would leave parts of the buffer around */
462 buffer.reserve(512);
463
464 for (;;) {
465 char ch = '\0';
466 auto got = read(input, &ch, 1);
467 if (got == 1 && ch != '\n' && ch != '\r') {
468 buffer.push_back(ch);
469 }
470 else {
471 break;
472 }
473 }
474
061bc5f0
RG
475 if (restoreTermSettings) {
476 tcsetattr(input, termAction, &oterm);
477 }
478
479 for (const auto& sig : signals) {
480 sigaction(sig.first, &sig.second, nullptr);
481 }
482
8a6030b6 483 return SensitiveData(std::move(buffer));
061bc5f0 484}