LCOV - code coverage report
Current view: top level - include/boost/http - bcrypt.hpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 94.2 % 86 81
Test Date: 2026-02-09 01:37:05 Functions: 96.3 % 27 26

            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
        

Generated by: LCOV version 2.3