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.5 % 91 86 5
Test Date: 2026-08-26 15:14:03 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                 :     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
        

Generated by: LCOV version 2.3