]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/credentials.cc
rec: allow exception to proxy protocal usage for specific listen addresses
[thirdparty/pdns.git] / pdns / credentials.cc
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
24 #include <cmath>
25 #include <stdexcept>
26
27 #ifdef HAVE_LIBSODIUM
28 #include <sodium.h>
29 #endif
30
31 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
32 #include <openssl/evp.h>
33 #include <openssl/kdf.h>
34 #include <openssl/opensslv.h>
35 #include <openssl/rand.h>
36 #endif
37
38 #include <fcntl.h>
39 #include <sys/stat.h>
40 #include <unistd.h>
41
42 #include "base64.hh"
43 #include "dns_random.hh"
44 #include "credentials.hh"
45 #include "misc.hh"
46
47 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
48 static size_t const pwhash_max_size = 128U; /* maximum size of the output */
49 static size_t const pwhash_output_size = 32U; /* size of the hashed output (before base64 encoding) */
50 static unsigned int const pwhash_salt_size = 16U; /* size of the salt (before base64 encoding */
51 static uint64_t const pwhash_max_work_factor = 32768U; /* max N for interactive login purposes */
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 */
55 static std::string const pwhash_prefix = "$scrypt$";
56 static size_t const pwhash_prefix_size = pwhash_prefix.size();
57 #endif
58
59 uint64_t const CredentialsHolder::s_defaultWorkFactor{1024U}; /* N */
60 uint64_t const CredentialsHolder::s_defaultParallelFactor{1U}; /* p */
61 uint64_t const CredentialsHolder::s_defaultBlockSize{8U}; /* r */
62
63 SensitiveData::SensitiveData(std::string&& data) :
64 d_data(std::move(data))
65 {
66 data.clear();
67 #ifdef HAVE_LIBSODIUM
68 sodium_mlock(d_data.data(), d_data.size());
69 #endif
70 }
71
72 SensitiveData& SensitiveData::operator=(SensitiveData&& rhs) noexcept
73 {
74 d_data = std::move(rhs.d_data);
75 rhs.clear();
76 return *this;
77 }
78
79 SensitiveData::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
87 SensitiveData::~SensitiveData()
88 {
89 clear();
90 }
91
92 void SensitiveData::clear()
93 {
94 #ifdef HAVE_LIBSODIUM
95 sodium_munlock(d_data.data(), d_data.size());
96 #endif
97 d_data.clear();
98 }
99
100 static 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)
101 {
102 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
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);
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
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) {
119 throw std::runtime_error("Error adding the password to the scrypt context to hash the supplied password");
120 }
121
122 if (EVP_PKEY_CTX_set1_scrypt_salt(pctx.get(), reinterpret_cast<const unsigned char*>(salt.data()), salt.size()) <= 0) {
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;
147 #else
148 throw std::runtime_error("Hashing support is not available");
149 #endif
150 }
151
152 static std::string generateRandomSalt()
153 {
154 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
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
169 std::string hashPassword([[maybe_unused]] const std::string& password, [[maybe_unused]] uint64_t workFactor, [[maybe_unused]] uint64_t parallelFactor, [[maybe_unused]] uint64_t blockSize)
170 {
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
176 std::string result;
177 result.reserve(pwhash_max_size);
178
179 result.append(pwhash_prefix);
180 result.append("ln=");
181 result.append(std::to_string(static_cast<uint64_t>(std::log2(workFactor))));
182 result.append(",p=");
183 result.append(std::to_string(parallelFactor));
184 result.append(",r=");
185 result.append(std::to_string(blockSize));
186 result.append("$");
187 auto salt = generateRandomSalt();
188 result.append(Base64Encode(salt));
189 result.append("$");
190
191 auto out = hashPasswordInternal(password, salt, workFactor, parallelFactor, blockSize);
192
193 result.append(Base64Encode(out));
194
195 return result;
196 #else
197 throw std::runtime_error("Hashing a password requires scrypt support in OpenSSL, and it is not available");
198 #endif
199 }
200
201 std::string hashPassword([[maybe_unused]] const std::string& password)
202 {
203 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
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
210 bool 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)
211 {
212 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
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 */
221 static 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)
222 {
223 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
224 auto parametersEnd = hash.find('$', pwhash_prefix.size());
225 if (parametersEnd == std::string::npos || parametersEnd == hash.size()) {
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 {
256 workFactor = pdns::checked_stoi<uint64_t>(parameters.at(0).substr(3));
257 workFactor = static_cast<uint64_t>(1) << workFactor;
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
262 parallelFactor = pdns::checked_stoi<uint64_t>(parameters.at(1).substr(2));
263 blockSize = pdns::checked_stoi<uint64_t>(parameters.at(2).substr(2));
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 }
283 #endif
284 }
285
286 bool verifyPassword(const std::string& hash, [[maybe_unused]] const std::string& password)
287 {
288 if (!isPasswordHashed(hash)) {
289 return false;
290 }
291
292 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
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);
303 #else
304 throw std::runtime_error("Verifying a hashed password requires scrypt support in OpenSSL, and it is not available");
305 #endif
306 }
307
308 bool isPasswordHashed([[maybe_unused]] const std::string& password)
309 {
310 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
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)) {
316 return false;
317 }
318
319 auto parametersEnd = password.find('$', pwhash_prefix.size());
320 if (parametersEnd == std::string::npos || parametersEnd == password.size()) {
321 return false;
322 }
323
324 size_t parametersSize = parametersEnd - pwhash_prefix.size();
325 /* ln=X,p=Y,r=Z */
326 if (parametersSize < 12) {
327 return false;
328 }
329
330 auto saltEnd = password.find('$', parametersEnd + 1);
331 if (saltEnd == std::string::npos || saltEnd == password.size()) {
332 return false;
333 }
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 */
341 if ((password.size() - saltEnd - 1) < pwhash_output_size) {
342 return false;
343 }
344
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 */
353 CredentialsHolder::CredentialsHolder(std::string&& password, bool hashPlaintext) :
354 d_credentials(std::move(password))
355 {
356 if (isHashingAvailable()) {
357 if (!isPasswordHashed(d_credentials.getString())) {
358 if (hashPlaintext) {
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));
364 d_isHashed = true;
365 }
366 }
367 else {
368 d_wasHashed = true;
369 d_isHashed = true;
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));
373 }
374 }
375
376 if (!d_isHashed) {
377 d_fallbackHashPerturb = dns_random_uint32();
378 d_fallbackHash = burtle(reinterpret_cast<const unsigned char*>(d_credentials.getString().data()), d_credentials.getString().size(), d_fallbackHashPerturb);
379 }
380 }
381
382 CredentialsHolder::~CredentialsHolder()
383 {
384 d_fallbackHashPerturb = 0;
385 d_fallbackHash = 0;
386 }
387
388 bool CredentialsHolder::matches(const std::string& password) const
389 {
390 if (d_isHashed) {
391 return verifyPassword(d_credentials.getString(), d_salt, d_workFactor, d_parallelFactor, d_blockSize, password);
392 }
393 else {
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
399 return constantTimeStringEquals(password, d_credentials.getString());
400 }
401 }
402
403 bool CredentialsHolder::isHashingAvailable()
404 {
405 #if !defined(DISABLE_HASHED_CREDENTIALS) && defined(HAVE_EVP_PKEY_CTX_SET1_SCRYPT_SALT)
406 return true;
407 #else
408 return false;
409 #endif
410 }
411
412 #include <csignal>
413 #include <termios.h>
414
415 SensitiveData CredentialsHolder::readFromTerminal()
416 {
417 struct termios term;
418 struct termios oterm;
419 bool restoreTermSettings = false;
420 int termAction = TCSAFLUSH;
421 #ifdef TCSASOFT
422 termAction |= TCSASOFT;
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));
436 restoreTermSettings = false;
437 }
438
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;
448 sa.sa_handler = [](int /* s */) {};
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
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
483 return SensitiveData(std::move(buffer));
484 }