include/boost/http/bcrypt.hpp

94.4% Lines (85/90) 96.3% List of functions (26/27)
bcrypt.hpp
f(x) Functions (27)
Function Calls Lines Blocks
boost::http::bcrypt::make_error_code(boost::http::bcrypt::error) :146 17x 100.0% 100.0% boost::http::bcrypt::result::result() :179 31x 100.0% 100.0% boost::http::bcrypt::result::str() const :188 30x 100.0% 100.0% boost::http::bcrypt::result::c_str() const :203 1x 100.0% 100.0% boost::http::bcrypt::result::size() const :219 7x 100.0% 100.0% boost::http::bcrypt::result::empty() const :227 4x 100.0% 100.0% boost::http::bcrypt::result::operator bool() const :235 2x 100.0% 100.0% boost::http::bcrypt::result::buf() :245 25x 100.0% 100.0% boost::http::bcrypt::result::set_size(unsigned char) :246 25x 100.0% 100.0% boost::http::bcrypt::detail::password_buf::password_buf(boost::core::basic_string_view<char>) :408 14x 100.0% 100.0% boost::http::bcrypt::detail::password_buf::operator boost::core::basic_string_view<char>() const :416 14x 100.0% 100.0% boost::http::bcrypt::detail::hash_buf::hash_buf(boost::core::basic_string_view<char>) :428 9x 100.0% 100.0% boost::http::bcrypt::detail::hash_buf::operator boost::core::basic_string_view<char>() const :437 9x 100.0% 100.0% boost::http::bcrypt::hash_task(boost::core::basic_string_view<char>, unsigned int, boost::http::bcrypt::version) :475 4x 100.0% 44.0% boost::http::bcrypt::compare_task(boost::core::basic_string_view<char>, boost::core::basic_string_view<char>) :504 6x 100.0% 44.0% boost::http::bcrypt::detail::hash_async_op::await_ready() const :530 1x 100.0% 100.0% boost::http::bcrypt::detail::hash_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :535 1x 100.0% 62.0% boost::http::bcrypt::detail::hash_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*)::{lambda(boost::http::bcrypt::result)#1}::operator()(boost::http::bcrypt::result) :544 1x 100.0% 100.0% boost::http::bcrypt::detail::hash_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*)::{lambda(std::__exception_ptr::exception_ptr)#1}::operator()(std::__exception_ptr::exception_ptr) :550 0 33.3% 0.0% boost::http::bcrypt::detail::hash_async_op::await_resume() :559 1x 75.0% 56.0% boost::http::bcrypt::detail::compare_async_op::await_ready() const :575 3x 100.0% 100.0% boost::http::bcrypt::detail::compare_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :580 3x 100.0% 65.0% boost::http::bcrypt::detail::compare_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*)::{lambda(bool)#1}::operator()(bool) :589 2x 100.0% 100.0% boost::http::bcrypt::detail::compare_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*)::{lambda(std::__exception_ptr::exception_ptr)#1}::operator()(std::__exception_ptr::exception_ptr) :595 1x 100.0% 100.0% boost::http::bcrypt::detail::compare_async_op::await_resume() :604 3x 100.0% 100.0% boost::http::bcrypt::hash_async(boost::core::basic_string_view<char>, unsigned int, boost::http::bcrypt::version) :639 1x 100.0% 100.0% boost::http::bcrypt::compare_async(boost::core::basic_string_view<char>, boost::core::basic_string_view<char>) :675 3x 100.0% 100.0%
Line TLA Hits 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 17x make_error_code(
147 error ev) noexcept
148 {
149 return system::error_code{
150 static_cast<std::underlying_type<
151 error>::type>(ev),
152 17x 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 31x result() noexcept
180 31x : size_(0)
181 {
182 31x buf_[0] = '\0';
183 31x }
184
185 /** Return the hash as a string_view.
186 */
187 core::string_view
188 30x str() const noexcept
189 {
190 30x 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 1x c_str() const noexcept
204 {
205 1x 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 7x size() const noexcept
220 {
221 7x return size_;
222 }
223
224 /** Check if result is empty.
225 */
226 bool
227 4x empty() const noexcept
228 {
229 4x return size_ == 0;
230 }
231
232 /** Check if result contains valid data.
233 */
234 explicit
235 2x operator bool() const noexcept
236 {
237 2x 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 25x char* buf() noexcept { return buf_; }
246 25x void set_size(unsigned char n) noexcept
247 {
248 25x size_ = n;
249 25x buf_[n] = '\0';
250 25x }
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 14x explicit password_buf(
409 core::string_view s) noexcept
410 28x : size_(static_cast<unsigned char>(
411 14x (std::min)(s.size(), std::size_t{72})))
412 {
413 14x std::memcpy(data_, s.data(), size_);
414 14x }
415
416 14x operator core::string_view() const noexcept
417 {
418 14x 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 9x explicit hash_buf(
429 core::string_view s) noexcept
430 18x : size_(static_cast<unsigned char>(
431 9x (std::min)(s.size(), std::size_t{60})))
432 {
433 9x std::memcpy(data_, s.data(), size_);
434 9x data_[size_] = '\0';
435 9x }
436
437 9x operator core::string_view() const noexcept
438 {
439 9x 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 4x 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 8x }
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 6x 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 12x }
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 1x bool await_ready() const noexcept
531 {
532 1x return false;
533 }
534
535 1x void await_suspend(
536 std::coroutine_handle<void> cont,
537 capy::io_env const* env)
538 {
539 1x cont_.h = cont;
540 1x auto caller_ex = env->executor;
541 1x auto& pool = capy::get_system_context();
542 1x auto sys_ex = pool.get_executor();
543 1x capy::run_async(sys_ex,
544 1x [this, caller_ex]
545 (result r) mutable
546 {
547 1x result_ = r;
548 1x caller_ex.dispatch(cont_).resume();
549 1x },
550 [this, caller_ex]
551 (std::exception_ptr ep) mutable
552 {
553 ep_ = ep;
554 caller_ex.dispatch(cont_).resume();
555 }
556 1x )(hash_task(password_, rounds_, ver_));
557 1x }
558
559 1x result await_resume()
560 {
561 1x if(ep_)
562 std::rethrow_exception(ep_);
563 1x 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 3x bool await_ready() const noexcept
576 {
577 3x return false;
578 }
579
580 3x void await_suspend(
581 std::coroutine_handle<void> cont,
582 capy::io_env const* env)
583 {
584 3x cont_.h = cont;
585 3x auto caller_ex = env->executor;
586 3x auto& pool = capy::get_system_context();
587 3x auto sys_ex = pool.get_executor();
588 3x capy::run_async(sys_ex,
589 2x [this, caller_ex]
590 (bool ok) mutable
591 {
592 2x result_ = ok;
593 2x caller_ex.dispatch(cont_).resume();
594 2x },
595 1x [this, caller_ex]
596 (std::exception_ptr ep) mutable
597 {
598 1x ep_ = ep;
599 1x caller_ex.dispatch(cont_).resume();
600 1x }
601 3x )(compare_task(password_, hash_str_));
602 3x }
603
604 3x bool await_resume()
605 {
606 3x if(ep_)
607 1x std::rethrow_exception(ep_);
608 2x 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 1x hash_async(
640 core::string_view password,
641 unsigned rounds = 10,
642 version ver = version::v2b)
643 {
644 1x return detail::hash_async_op{
645 detail::password_buf(password),
646 rounds,
647 ver,
648 {},
649 {},
650 1x {}};
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 3x compare_async(
676 core::string_view password,
677 core::string_view hash_str)
678 {
679 3x return detail::compare_async_op{
680 detail::password_buf(password),
681 detail::hash_buf(hash_str),
682 false,
683 {},
684 3x {}};
685 }
686
687 } // bcrypt
688 } // http
689 } // boost
690
691 #endif
692