include/boost/http/bcrypt.hpp

94.5% Lines (86/0/91) 96.3% List of functions (26/0/27)
bcrypt.hpp
f(x) Functions (27)
Function Calls Lines Blocks
boost::http::bcrypt::make_error_code(boost::http::bcrypt::error) :128 17x 100.0% 100.0% boost::http::bcrypt::result::result() :161 31x 100.0% 100.0% boost::http::bcrypt::result::str() const :170 30x 100.0% 100.0% boost::http::bcrypt::result::c_str() const :185 1x 100.0% 100.0% boost::http::bcrypt::result::size() const :201 7x 100.0% 100.0% boost::http::bcrypt::result::empty() const :209 4x 100.0% 100.0% boost::http::bcrypt::result::operator bool() const :217 2x 100.0% 100.0% boost::http::bcrypt::result::buf() :227 25x 100.0% 100.0% boost::http::bcrypt::result::set_size(unsigned char) :228 25x 100.0% 100.0% boost::http::bcrypt::detail::password_buf::password_buf(boost::core::basic_string_view<char>) :390 14x 100.0% 100.0% boost::http::bcrypt::detail::password_buf::operator boost::core::basic_string_view<char>() const :398 14x 100.0% 100.0% boost::http::bcrypt::detail::hash_buf::hash_buf(boost::core::basic_string_view<char>) :410 9x 100.0% 100.0% boost::http::bcrypt::detail::hash_buf::operator boost::core::basic_string_view<char>() const :419 9x 100.0% 100.0% boost::http::bcrypt::hash_task(boost::core::basic_string_view<char>, unsigned int, boost::http::bcrypt::version) :457 4x 100.0% 44.0% boost::http::bcrypt::compare_task(boost::core::basic_string_view<char>, boost::core::basic_string_view<char>) :486 6x 100.0% 44.0% boost::http::bcrypt::detail::hash_async_op::await_ready() const :512 1x 100.0% 100.0% boost::http::bcrypt::detail::hash_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :517 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) :526 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) :532 0 33.3% 0.0% boost::http::bcrypt::detail::hash_async_op::await_resume() :541 1x 75.0% 56.0% boost::http::bcrypt::detail::compare_async_op::await_ready() const :557 3x 100.0% 100.0% boost::http::bcrypt::detail::compare_async_op::await_suspend(std::__n4861::coroutine_handle<void>, boost::capy::io_env const*) :562 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) :571 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) :577 1x 100.0% 100.0% boost::http::bcrypt::detail::compare_async_op::await_resume() :586 3x 100.0% 100.0% boost::http::bcrypt::hash_async(boost::core::basic_string_view<char>, unsigned int, boost::http::bcrypt::version) :621 1x 100.0% 100.0% boost::http::bcrypt::compare_async(boost::core::basic_string_view<char>, boost::core::basic_string_view<char>) :657 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 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 17x make_error_code(
129 error ev) noexcept
130 {
131 17x return std::error_code{
132 static_cast<std::underlying_type<
133 error>::type>(ev),
134 17x 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 31x result() noexcept
162 31x : size_(0)
163 {
164 31x buf_[0] = '\0';
165 31x }
166
167 /** Return the hash as a string_view.
168 */
169 core::string_view
170 30x str() const noexcept
171 {
172 30x 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 1x c_str() const noexcept
186 {
187 1x 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 7x size() const noexcept
202 {
203 7x return size_;
204 }
205
206 /** Check if result is empty.
207 */
208 bool
209 4x empty() const noexcept
210 {
211 4x return size_ == 0;
212 }
213
214 /** Check if result contains valid data.
215 */
216 explicit
217 2x operator bool() const noexcept
218 {
219 2x 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 25x char* buf() noexcept { return buf_; }
228 25x void set_size(unsigned char n) noexcept
229 {
230 25x size_ = n;
231 25x buf_[n] = '\0';
232 25x }
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 14x explicit password_buf(
391 core::string_view s) noexcept
392 28x : size_(static_cast<unsigned char>(
393 14x (std::min)(s.size(), std::size_t{72})))
394 {
395 14x std::memcpy(data_, s.data(), size_);
396 14x }
397
398 14x operator core::string_view() const noexcept
399 {
400 14x 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 9x explicit hash_buf(
411 core::string_view s) noexcept
412 18x : size_(static_cast<unsigned char>(
413 9x (std::min)(s.size(), std::size_t{60})))
414 {
415 9x std::memcpy(data_, s.data(), size_);
416 9x data_[size_] = '\0';
417 9x }
418
419 9x operator core::string_view() const noexcept
420 {
421 9x 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 4x 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 8x }
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 6x 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 12x }
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 1x bool await_ready() const noexcept
513 {
514 1x return false;
515 }
516
517 1x void await_suspend(
518 std::coroutine_handle<void> cont,
519 capy::io_env const* env)
520 {
521 1x cont_.h = cont;
522 1x auto caller_ex = env->executor;
523 1x auto& pool = capy::get_system_context();
524 1x auto sys_ex = pool.get_executor();
525 1x capy::run_async(sys_ex,
526 1x [this, caller_ex]
527 (result r) mutable
528 {
529 1x result_ = r;
530 1x caller_ex.dispatch(cont_).resume();
531 1x },
532 [this, caller_ex]
533 (std::exception_ptr ep) mutable
534 {
535 ep_ = ep;
536 caller_ex.dispatch(cont_).resume();
537 }
538 1x )(hash_task(password_, rounds_, ver_));
539 1x }
540
541 1x result await_resume()
542 {
543 1x if(ep_)
544 std::rethrow_exception(ep_);
545 1x 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 3x bool await_ready() const noexcept
558 {
559 3x return false;
560 }
561
562 3x void await_suspend(
563 std::coroutine_handle<void> cont,
564 capy::io_env const* env)
565 {
566 3x cont_.h = cont;
567 3x auto caller_ex = env->executor;
568 3x auto& pool = capy::get_system_context();
569 3x auto sys_ex = pool.get_executor();
570 3x capy::run_async(sys_ex,
571 2x [this, caller_ex]
572 (bool ok) mutable
573 {
574 2x result_ = ok;
575 2x caller_ex.dispatch(cont_).resume();
576 2x },
577 1x [this, caller_ex]
578 (std::exception_ptr ep) mutable
579 {
580 1x ep_ = ep;
581 1x caller_ex.dispatch(cont_).resume();
582 1x }
583 3x )(compare_task(password_, hash_str_));
584 3x }
585
586 3x bool await_resume()
587 {
588 3x if(ep_)
589 1x std::rethrow_exception(ep_);
590 2x 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 1x hash_async(
622 core::string_view password,
623 unsigned rounds = 10,
624 version ver = version::v2b)
625 {
626 1x return detail::hash_async_op{
627 detail::password_buf(password),
628 rounds,
629 ver,
630 {},
631 {},
632 1x {}};
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 3x compare_async(
658 core::string_view password,
659 core::string_view hash_str)
660 {
661 3x return detail::compare_async_op{
662 detail::password_buf(password),
663 detail::hash_buf(hash_str),
664 false,
665 {},
666 3x {}};
667 }
668
669 } // bcrypt
670 } // http
671 } // boost
672
673 #endif
674