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