TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/http
8 : //
9 :
10 : /** @file
11 : bcrypt password hashing library.
12 :
13 : This header provides bcrypt password hashing with three API tiers:
14 :
15 : **Tier 1 -- Synchronous** (low-level, no capy dependency):
16 : @code
17 : bcrypt::result r = bcrypt::hash("password", 12);
18 : std::error_code ec;
19 : bool ok = bcrypt::compare("password", r.str(), ec);
20 : @endcode
21 :
22 : **Tier 2 -- Capy Task** (lazy coroutine, caller controls executor):
23 : @code
24 : auto r = co_await bcrypt::hash_task("password", 12);
25 : @endcode
26 :
27 : **Tier 3 -- Friendly Async** (auto-offloads to system thread pool):
28 : @code
29 : auto r = co_await bcrypt::hash_async("password", 12);
30 : bool ok = co_await bcrypt::compare_async("password", r.str());
31 : @endcode
32 : */
33 :
34 : #ifndef BOOST_HTTP_BCRYPT_HPP
35 : #define BOOST_HTTP_BCRYPT_HPP
36 :
37 : #include <boost/http/detail/config.hpp>
38 : #include <boost/http/detail/except.hpp>
39 : #include <boost/core/detail/string_view.hpp>
40 :
41 : #include <boost/capy/continuation.hpp>
42 : #include <boost/capy/task.hpp>
43 : #include <boost/capy/ex/executor_ref.hpp>
44 : #include <boost/capy/ex/io_env.hpp>
45 : #include <boost/capy/ex/run_async.hpp>
46 : #include <boost/capy/ex/system_context.hpp>
47 :
48 : #include <cstddef>
49 : #include <cstring>
50 : #include <exception>
51 : #include <string>
52 : #include <system_error>
53 :
54 : namespace boost {
55 : namespace http {
56 : namespace bcrypt {
57 :
58 : //------------------------------------------------
59 :
60 : /** bcrypt hash version prefix.
61 :
62 : The version determines which variant of bcrypt is used.
63 : All versions produce compatible hashes.
64 : */
65 : enum class version
66 : {
67 : /// $2a$ - Original specification
68 : v2a,
69 :
70 : /// $2b$ - Fixed handling of passwords > 255 chars (recommended)
71 : v2b
72 : };
73 :
74 : //------------------------------------------------
75 :
76 : /** Error codes for bcrypt operations.
77 :
78 : These errors indicate malformed input from untrusted sources.
79 : */
80 : enum class error
81 : {
82 : /// Success
83 : ok = 0,
84 :
85 : /// Salt string is malformed
86 : invalid_salt,
87 :
88 : /// Hash string is malformed
89 : invalid_hash
90 : };
91 :
92 : } // bcrypt
93 : } // http
94 :
95 : } // boost
96 :
97 : namespace std {
98 : template<>
99 : struct is_error_code_enum<
100 : ::boost::http::bcrypt::error>
101 : : std::true_type {};
102 : } // std
103 :
104 : namespace boost {
105 : namespace http {
106 : namespace bcrypt {
107 :
108 : namespace detail {
109 :
110 : struct BOOST_SYMBOL_VISIBLE
111 : error_cat_type
112 : : std::error_category
113 : {
114 : BOOST_HTTP_DECL const char* name(
115 : ) const noexcept override;
116 : BOOST_HTTP_DECL std::string message(
117 : int) const override;
118 : constexpr error_cat_type() noexcept = default;
119 : };
120 :
121 : BOOST_HTTP_DECL extern
122 : error_cat_type error_cat;
123 :
124 : } // detail
125 :
126 : inline
127 : std::error_code
128 HIT 17 : make_error_code(
129 : error ev) noexcept
130 : {
131 17 : return std::error_code{
132 : static_cast<std::underlying_type<
133 : error>::type>(ev),
134 17 : detail::error_cat};
135 : }
136 :
137 : //------------------------------------------------
138 :
139 : /** Fixed-size buffer for bcrypt hash output.
140 :
141 : Stores a bcrypt hash string (max 60 chars) in an
142 : inline buffer with no heap allocation.
143 :
144 : @par Example
145 : @code
146 : bcrypt::result r = bcrypt::hash("password", 10);
147 : core::string_view sv = r; // or r.str()
148 : std::cout << r.c_str(); // null-terminated
149 : @endcode
150 : */
151 : class result
152 : {
153 : char buf_[61];
154 : unsigned char size_;
155 :
156 : public:
157 : /** Default constructor.
158 :
159 : Constructs an empty result.
160 : */
161 31 : result() noexcept
162 31 : : size_(0)
163 : {
164 31 : buf_[0] = '\0';
165 31 : }
166 :
167 : /** Return the hash as a string_view.
168 : */
169 : core::string_view
170 30 : str() const noexcept
171 : {
172 30 : return core::string_view(buf_, size_);
173 : }
174 :
175 : /** Implicit conversion to string_view.
176 : */
177 : operator core::string_view() const noexcept
178 : {
179 : return str();
180 : }
181 :
182 : /** Return null-terminated C string.
183 : */
184 : char const*
185 1 : c_str() const noexcept
186 : {
187 1 : return buf_;
188 : }
189 :
190 : /** Return pointer to data.
191 : */
192 : char const*
193 : data() const noexcept
194 : {
195 : return buf_;
196 : }
197 :
198 : /** Return size in bytes (excludes null terminator).
199 : */
200 : std::size_t
201 7 : size() const noexcept
202 : {
203 7 : return size_;
204 : }
205 :
206 : /** Check if result is empty.
207 : */
208 : bool
209 4 : empty() const noexcept
210 : {
211 4 : return size_ == 0;
212 : }
213 :
214 : /** Check if result contains valid data.
215 : */
216 : explicit
217 2 : operator bool() const noexcept
218 : {
219 2 : return size_ != 0;
220 : }
221 :
222 : private:
223 : friend BOOST_HTTP_DECL result gen_salt(unsigned, version);
224 : friend BOOST_HTTP_DECL result hash(core::string_view, unsigned, version);
225 : friend BOOST_HTTP_DECL result hash(core::string_view, core::string_view, std::error_code&);
226 :
227 25 : char* buf() noexcept { return buf_; }
228 25 : void set_size(unsigned char n) noexcept
229 : {
230 25 : size_ = n;
231 25 : buf_[n] = '\0';
232 25 : }
233 : };
234 :
235 : //------------------------------------------------
236 :
237 : /** Generate a random salt.
238 :
239 : Creates a bcrypt salt string suitable for use with
240 : the hash() function.
241 :
242 : @par Preconditions
243 : @code
244 : rounds >= 4 && rounds <= 31
245 : @endcode
246 :
247 : @par Exception Safety
248 : Strong guarantee.
249 :
250 : @par Complexity
251 : Constant.
252 :
253 : @param rounds Cost factor. Each increment doubles the work.
254 : Default is 10, which takes approximately 100ms on modern hardware.
255 :
256 : @param ver Hash version to use.
257 :
258 : @return A 29-character salt string.
259 :
260 : @throws std::invalid_argument if rounds is out of range.
261 : @throws system_error on RNG failure.
262 : */
263 : BOOST_HTTP_DECL
264 : result
265 : gen_salt(
266 : unsigned rounds = 10,
267 : version ver = version::v2b);
268 :
269 : /** Hash a password with auto-generated salt.
270 :
271 : Generates a random salt and hashes the password.
272 :
273 : @par Preconditions
274 : @code
275 : rounds >= 4 && rounds <= 31
276 : @endcode
277 :
278 : @par Exception Safety
279 : Strong guarantee.
280 :
281 : @par Complexity
282 : O(2^rounds).
283 :
284 : @param password The password to hash. Only the first 72 bytes
285 : are used (bcrypt limitation).
286 :
287 : @param rounds Cost factor. Each increment doubles the work.
288 :
289 : @param ver Hash version to use.
290 :
291 : @return A 60-character hash string.
292 :
293 : @throws std::invalid_argument if rounds is out of range.
294 : @throws system_error on RNG failure.
295 : */
296 : BOOST_HTTP_DECL
297 : result
298 : hash(
299 : core::string_view password,
300 : unsigned rounds = 10,
301 : version ver = version::v2b);
302 :
303 : /** Hash a password using a provided salt.
304 :
305 : Uses the given salt to hash the password. The salt should
306 : be a string previously returned by gen_salt() or extracted
307 : from a hash string.
308 :
309 : @par Exception Safety
310 : Strong guarantee.
311 :
312 : @par Complexity
313 : O(2^rounds).
314 :
315 : @param password The password to hash.
316 :
317 : @param salt The salt string (29 characters).
318 :
319 : @param ec Set to bcrypt::error::invalid_salt if the salt
320 : is malformed.
321 :
322 : @return A 60-character hash string, or empty result on error.
323 : */
324 : BOOST_HTTP_DECL
325 : result
326 : hash(
327 : core::string_view password,
328 : core::string_view salt,
329 : std::error_code& ec);
330 :
331 : /** Compare a password against a hash.
332 :
333 : Extracts the salt from the hash, re-hashes the password,
334 : and compares the result.
335 :
336 : @par Exception Safety
337 : Strong guarantee.
338 :
339 : @par Complexity
340 : O(2^rounds).
341 :
342 : @param password The plaintext password to check.
343 :
344 : @param hash The hash string to compare against.
345 :
346 : @param ec Set to bcrypt::error::invalid_hash if the hash
347 : is malformed.
348 :
349 : @return true if the password matches the hash, false if
350 : it does not match OR if an error occurred. Always check
351 : ec to distinguish between a mismatch and an error.
352 : */
353 : BOOST_HTTP_DECL
354 : bool
355 : compare(
356 : core::string_view password,
357 : core::string_view hash,
358 : std::error_code& ec);
359 :
360 : /** Extract the cost factor from a hash string.
361 :
362 : @par Exception Safety
363 : Strong guarantee.
364 :
365 : @par Complexity
366 : Constant.
367 :
368 : @param hash The hash string to parse.
369 :
370 : @param ec Set to bcrypt::error::invalid_hash if the hash
371 : is malformed.
372 :
373 : @return The cost factor (4-31) on success, or 0 if an
374 : error occurred.
375 : */
376 : BOOST_HTTP_DECL
377 : unsigned
378 : get_rounds(
379 : core::string_view hash,
380 : std::error_code& ec);
381 :
382 : namespace detail {
383 :
384 : // bcrypt truncates passwords to 72 bytes
385 : struct password_buf
386 : {
387 : char data_[72];
388 : unsigned char size_;
389 :
390 14 : explicit password_buf(
391 : core::string_view s) noexcept
392 28 : : size_(static_cast<unsigned char>(
393 14 : (std::min)(s.size(), std::size_t{72})))
394 : {
395 14 : std::memcpy(data_, s.data(), size_);
396 14 : }
397 :
398 14 : operator core::string_view() const noexcept
399 : {
400 14 : return {data_, size_};
401 : }
402 : };
403 :
404 : // bcrypt hashes are always 60 characters
405 : struct hash_buf
406 : {
407 : char data_[61];
408 : unsigned char size_;
409 :
410 9 : explicit hash_buf(
411 : core::string_view s) noexcept
412 18 : : size_(static_cast<unsigned char>(
413 9 : (std::min)(s.size(), std::size_t{60})))
414 : {
415 9 : std::memcpy(data_, s.data(), size_);
416 9 : data_[size_] = '\0';
417 9 : }
418 :
419 9 : operator core::string_view() const noexcept
420 : {
421 9 : return {data_, size_};
422 : }
423 : };
424 :
425 : } // detail
426 :
427 : //------------------------------------------------
428 :
429 : /** Hash a password, returning a lazy task.
430 :
431 : Returns a @ref capy::task that wraps the synchronous
432 : hash() call. The caller can co_await this task directly
433 : or launch it on a specific executor via run_async().
434 :
435 : @par Example
436 : @code
437 : // co_await in current context
438 : bcrypt::result r = co_await bcrypt::hash_task("password", 12);
439 :
440 : // or launch on a specific executor
441 : run_async(my_executor)(bcrypt::hash_task("password", 12));
442 : @endcode
443 :
444 : @param password The password to hash.
445 :
446 : @param rounds Cost factor. Each increment doubles the work.
447 :
448 : @param ver Hash version to use.
449 :
450 : @return A lazy task yielding `result`.
451 :
452 : @throws std::invalid_argument if rounds is out of range.
453 : @throws system_error on RNG failure.
454 : */
455 : inline
456 : capy::task<result>
457 4 : hash_task(
458 : core::string_view password,
459 : unsigned rounds = 10,
460 : version ver = version::v2b)
461 : {
462 : detail::password_buf pw(password);
463 : co_return hash(pw, rounds, ver);
464 8 : }
465 :
466 : /** Compare a password against a hash, returning a lazy task.
467 :
468 : Returns a @ref capy::task that wraps the synchronous
469 : compare() call. Errors are translated to exceptions.
470 :
471 : @par Example
472 : @code
473 : bool ok = co_await bcrypt::compare_task("password", stored_hash);
474 : @endcode
475 :
476 : @param password The plaintext password to check.
477 :
478 : @param hash_str The hash string to compare against.
479 :
480 : @return A lazy task yielding `bool`.
481 :
482 : @throws system_error if the hash is malformed.
483 : */
484 : inline
485 : capy::task<bool>
486 6 : compare_task(
487 : core::string_view password,
488 : core::string_view hash_str)
489 : {
490 : detail::password_buf pw(password);
491 : detail::hash_buf hs(hash_str);
492 : std::error_code ec;
493 : bool ok = compare(pw, hs, ec);
494 : if(ec)
495 : http::detail::throw_system_error(ec);
496 : co_return ok;
497 12 : }
498 :
499 : //------------------------------------------------
500 :
501 : namespace detail {
502 :
503 : struct hash_async_op
504 : {
505 : password_buf password_;
506 : unsigned rounds_;
507 : version ver_;
508 : result result_;
509 : std::exception_ptr ep_;
510 : capy::continuation cont_;
511 :
512 1 : bool await_ready() const noexcept
513 : {
514 1 : return false;
515 : }
516 :
517 1 : void await_suspend(
518 : std::coroutine_handle<void> cont,
519 : capy::io_env const* env)
520 : {
521 1 : cont_.h = cont;
522 1 : auto caller_ex = env->executor;
523 1 : auto& pool = capy::get_system_context();
524 1 : auto sys_ex = pool.get_executor();
525 1 : capy::run_async(sys_ex,
526 1 : [this, caller_ex]
527 : (result r) mutable
528 : {
529 1 : result_ = r;
530 1 : caller_ex.dispatch(cont_).resume();
531 1 : },
532 MIS 0 : [this, caller_ex]
533 : (std::exception_ptr ep) mutable
534 : {
535 0 : ep_ = ep;
536 0 : caller_ex.dispatch(cont_).resume();
537 0 : }
538 HIT 1 : )(hash_task(password_, rounds_, ver_));
539 1 : }
540 :
541 1 : result await_resume()
542 : {
543 1 : if(ep_)
544 MIS 0 : std::rethrow_exception(ep_);
545 HIT 1 : return result_;
546 : }
547 : };
548 :
549 : struct compare_async_op
550 : {
551 : password_buf password_;
552 : hash_buf hash_str_;
553 : bool result_ = false;
554 : std::exception_ptr ep_;
555 : capy::continuation cont_;
556 :
557 3 : bool await_ready() const noexcept
558 : {
559 3 : return false;
560 : }
561 :
562 3 : void await_suspend(
563 : std::coroutine_handle<void> cont,
564 : capy::io_env const* env)
565 : {
566 3 : cont_.h = cont;
567 3 : auto caller_ex = env->executor;
568 3 : auto& pool = capy::get_system_context();
569 3 : auto sys_ex = pool.get_executor();
570 3 : capy::run_async(sys_ex,
571 2 : [this, caller_ex]
572 : (bool ok) mutable
573 : {
574 2 : result_ = ok;
575 2 : caller_ex.dispatch(cont_).resume();
576 2 : },
577 1 : [this, caller_ex]
578 : (std::exception_ptr ep) mutable
579 : {
580 1 : ep_ = ep;
581 1 : caller_ex.dispatch(cont_).resume();
582 1 : }
583 3 : )(compare_task(password_, hash_str_));
584 3 : }
585 :
586 3 : bool await_resume()
587 : {
588 3 : if(ep_)
589 1 : std::rethrow_exception(ep_);
590 2 : return result_;
591 : }
592 : };
593 :
594 : } // detail
595 :
596 : /** Hash a password asynchronously on the system thread pool.
597 :
598 : Returns an awaitable that offloads the CPU-intensive
599 : bcrypt work to the system thread pool, then resumes
600 : the caller on their original executor. Modeled after
601 : Express.js: `await bcrypt.hash(password, 12)`.
602 :
603 : @par Example
604 : @code
605 : bcrypt::result r = co_await bcrypt::hash_async("my_password", 12);
606 : @endcode
607 :
608 : @param password The password to hash.
609 :
610 : @param rounds Cost factor. Each increment doubles the work.
611 :
612 : @param ver Hash version to use.
613 :
614 : @return An awaitable yielding `result`.
615 :
616 : @throws std::invalid_argument if rounds is out of range.
617 : @throws system_error on RNG failure.
618 : */
619 : inline
620 : detail::hash_async_op
621 1 : hash_async(
622 : core::string_view password,
623 : unsigned rounds = 10,
624 : version ver = version::v2b)
625 : {
626 1 : return detail::hash_async_op{
627 : detail::password_buf(password),
628 : rounds,
629 : ver,
630 : {},
631 : {},
632 1 : {}};
633 : }
634 :
635 : /** Compare a password against a hash asynchronously.
636 :
637 : Returns an awaitable that offloads the CPU-intensive
638 : bcrypt work to the system thread pool, then resumes
639 : the caller on their original executor. Modeled after
640 : Express.js: `await bcrypt.compare(password, hash)`.
641 :
642 : @par Example
643 : @code
644 : bool ok = co_await bcrypt::compare_async("my_password", stored_hash);
645 : @endcode
646 :
647 : @param password The plaintext password to check.
648 :
649 : @param hash_str The hash string to compare against.
650 :
651 : @return An awaitable yielding `bool`.
652 :
653 : @throws system_error if the hash is malformed.
654 : */
655 : inline
656 : detail::compare_async_op
657 3 : compare_async(
658 : core::string_view password,
659 : core::string_view hash_str)
660 : {
661 3 : return detail::compare_async_op{
662 : detail::password_buf(password),
663 : detail::hash_buf(hash_str),
664 : false,
665 : {},
666 3 : {}};
667 : }
668 :
669 : } // bcrypt
670 : } // http
671 : } // boost
672 :
673 : #endif
|