LCOV - code coverage report
Current view: top level - include/boost/http - bcrypt.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 94.4 % 90 85 5
Test Date: 2026-06-13 19:44:58 Functions: 96.3 % 27 26 1

           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                 :     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/continuation.hpp>
      45                 : #include <boost/capy/task.hpp>
      46                 : #include <boost/capy/ex/executor_ref.hpp>
      47                 : #include <boost/capy/ex/io_env.hpp>
      48                 : #include <boost/capy/ex/run_async.hpp>
      49                 : #include <boost/capy/ex/system_context.hpp>
      50                 : 
      51                 : #include <cstddef>
      52                 : #include <cstring>
      53                 : #include <exception>
      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 HIT          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                 :     capy::continuation cont_;
     529                 : 
     530               1 :     bool await_ready() const noexcept
     531                 :     {
     532               1 :         return false;
     533                 :     }
     534                 : 
     535               1 :     void await_suspend(
     536                 :         std::coroutine_handle<void> cont,
     537                 :         capy::io_env const* env)
     538                 :     {
     539               1 :         cont_.h = cont;
     540               1 :         auto caller_ex = env->executor;
     541               1 :         auto& pool = capy::get_system_context();
     542               1 :         auto sys_ex = pool.get_executor();
     543               1 :         capy::run_async(sys_ex,
     544               1 :             [this, caller_ex]
     545                 :             (result r) mutable
     546                 :             {
     547               1 :                 result_ = r;
     548               1 :                 caller_ex.dispatch(cont_).resume();
     549               1 :             },
     550 MIS           0 :             [this, caller_ex]
     551                 :             (std::exception_ptr ep) mutable
     552                 :             {
     553               0 :                 ep_ = ep;
     554               0 :                 caller_ex.dispatch(cont_).resume();
     555               0 :             }
     556 HIT           1 :         )(hash_task(password_, rounds_, ver_));
     557               1 :     }
     558                 : 
     559               1 :     result await_resume()
     560                 :     {
     561               1 :         if(ep_)
     562 MIS           0 :             std::rethrow_exception(ep_);
     563 HIT           1 :         return result_;
     564                 :     }
     565                 : };
     566                 : 
     567                 : struct compare_async_op
     568                 : {
     569                 :     password_buf password_;
     570                 :     hash_buf hash_str_;
     571                 :     bool result_ = false;
     572                 :     std::exception_ptr ep_;
     573                 :     capy::continuation cont_;
     574                 : 
     575               3 :     bool await_ready() const noexcept
     576                 :     {
     577               3 :         return false;
     578                 :     }
     579                 : 
     580               3 :     void await_suspend(
     581                 :         std::coroutine_handle<void> cont,
     582                 :         capy::io_env const* env)
     583                 :     {
     584               3 :         cont_.h = cont;
     585               3 :         auto caller_ex = env->executor;
     586               3 :         auto& pool = capy::get_system_context();
     587               3 :         auto sys_ex = pool.get_executor();
     588               3 :         capy::run_async(sys_ex,
     589               2 :             [this, caller_ex]
     590                 :             (bool ok) mutable
     591                 :             {
     592               2 :                 result_ = ok;
     593               2 :                 caller_ex.dispatch(cont_).resume();
     594               2 :             },
     595               1 :             [this, caller_ex]
     596                 :             (std::exception_ptr ep) mutable
     597                 :             {
     598               1 :                 ep_ = ep;
     599               1 :                 caller_ex.dispatch(cont_).resume();
     600               1 :             }
     601               3 :         )(compare_task(password_, hash_str_));
     602               3 :     }
     603                 : 
     604               3 :     bool await_resume()
     605                 :     {
     606               3 :         if(ep_)
     607               1 :             std::rethrow_exception(ep_);
     608               2 :         return result_;
     609                 :     }
     610                 : };
     611                 : 
     612                 : } // detail
     613                 : 
     614                 : /** Hash a password asynchronously on the system thread pool.
     615                 : 
     616                 :     Returns an awaitable that offloads the CPU-intensive
     617                 :     bcrypt work to the system thread pool, then resumes
     618                 :     the caller on their original executor. Modeled after
     619                 :     Express.js: `await bcrypt.hash(password, 12)`.
     620                 : 
     621                 :     @par Example
     622                 :     @code
     623                 :     bcrypt::result r = co_await bcrypt::hash_async("my_password", 12);
     624                 :     @endcode
     625                 : 
     626                 :     @param password The password to hash.
     627                 : 
     628                 :     @param rounds Cost factor. Each increment doubles the work.
     629                 : 
     630                 :     @param ver Hash version to use.
     631                 : 
     632                 :     @return An awaitable yielding `result`.
     633                 : 
     634                 :     @throws std::invalid_argument if rounds is out of range.
     635                 :     @throws system_error on RNG failure.
     636                 : */
     637                 : inline
     638                 : detail::hash_async_op
     639               1 : hash_async(
     640                 :     core::string_view password,
     641                 :     unsigned rounds = 10,
     642                 :     version ver = version::v2b)
     643                 : {
     644               1 :     return detail::hash_async_op{
     645                 :         detail::password_buf(password),
     646                 :         rounds,
     647                 :         ver,
     648                 :         {},
     649                 :         {},
     650               1 :         {}};
     651                 : }
     652                 : 
     653                 : /** Compare a password against a hash asynchronously.
     654                 : 
     655                 :     Returns an awaitable that offloads the CPU-intensive
     656                 :     bcrypt work to the system thread pool, then resumes
     657                 :     the caller on their original executor. Modeled after
     658                 :     Express.js: `await bcrypt.compare(password, hash)`.
     659                 : 
     660                 :     @par Example
     661                 :     @code
     662                 :     bool ok = co_await bcrypt::compare_async("my_password", stored_hash);
     663                 :     @endcode
     664                 : 
     665                 :     @param password The plaintext password to check.
     666                 : 
     667                 :     @param hash_str The hash string to compare against.
     668                 : 
     669                 :     @return An awaitable yielding `bool`.
     670                 : 
     671                 :     @throws system_error if the hash is malformed.
     672                 : */
     673                 : inline
     674                 : detail::compare_async_op
     675               3 : compare_async(
     676                 :     core::string_view password,
     677                 :     core::string_view hash_str)
     678                 : {
     679               3 :     return detail::compare_async_op{
     680                 :         detail::password_buf(password),
     681                 :         detail::hash_buf(hash_str),
     682                 :         false,
     683                 :         {},
     684               3 :         {}};
     685                 : }
     686                 : 
     687                 : } // bcrypt
     688                 : } // http
     689                 : } // boost
     690                 : 
     691                 : #endif
        

Generated by: LCOV version 2.3