libs/http/include/boost/http/bcrypt.hpp

94.2% Lines (81/86) 96.3% Functions (26/27) 91.7% Branches (11/12)
libs/http/include/boost/http/bcrypt.hpp
Line Branch 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/coro.hpp>
45 #include <boost/capy/task.hpp>
46 #include <boost/capy/ex/executor_ref.hpp>
47 #include <boost/capy/ex/run_async.hpp>
48 #include <boost/capy/ex/system_context.hpp>
49
50 #include <cstddef>
51 #include <cstring>
52 #include <exception>
53 #include <stop_token>
54 #include <string>
55 #include <system_error>
56
57 namespace boost {
58 namespace http {
59 namespace bcrypt {
60
61 //------------------------------------------------
62
63 /** bcrypt hash version prefix.
64
65 The version determines which variant of bcrypt is used.
66 All versions produce compatible hashes.
67 */
68 enum class version
69 {
70 /// $2a$ - Original specification
71 v2a,
72
73 /// $2b$ - Fixed handling of passwords > 255 chars (recommended)
74 v2b
75 };
76
77 //------------------------------------------------
78
79 /** Error codes for bcrypt operations.
80
81 These errors indicate malformed input from untrusted sources.
82 */
83 enum class error
84 {
85 /// Success
86 ok = 0,
87
88 /// Salt string is malformed
89 invalid_salt,
90
91 /// Hash string is malformed
92 invalid_hash
93 };
94
95 } // bcrypt
96 } // http
97
98 namespace system {
99 template<>
100 struct is_error_code_enum<
101 ::boost::http::bcrypt::error>
102 {
103 static bool const value = true;
104 };
105 } // system
106 } // boost
107
108 namespace std {
109 template<>
110 struct is_error_code_enum<
111 ::boost::http::bcrypt::error>
112 : std::true_type {};
113 } // std
114
115 namespace boost {
116 namespace http {
117 namespace bcrypt {
118
119 namespace detail {
120
121 struct BOOST_SYMBOL_VISIBLE
122 error_cat_type
123 : system::error_category
124 {
125 BOOST_HTTP_DECL const char* name(
126 ) const noexcept override;
127 BOOST_HTTP_DECL std::string message(
128 int) const override;
129 BOOST_HTTP_DECL char const* message(
130 int, char*, std::size_t
131 ) const noexcept override;
132 BOOST_SYSTEM_CONSTEXPR error_cat_type()
133 : error_category(0xbc8f2a4e7c193d56)
134 {
135 }
136 };
137
138 BOOST_HTTP_DECL extern
139 error_cat_type error_cat;
140
141 } // detail
142
143 inline
144 BOOST_SYSTEM_CONSTEXPR
145 system::error_code
146 17 make_error_code(
147 error ev) noexcept
148 {
149 return system::error_code{
150 static_cast<std::underlying_type<
151 error>::type>(ev),
152 17 detail::error_cat};
153 }
154
155 //------------------------------------------------
156
157 /** Fixed-size buffer for bcrypt hash output.
158
159 Stores a bcrypt hash string (max 60 chars) in an
160 inline buffer with no heap allocation.
161
162 @par Example
163 @code
164 bcrypt::result r = bcrypt::hash("password", 10);
165 core::string_view sv = r; // or r.str()
166 std::cout << r.c_str(); // null-terminated
167 @endcode
168 */
169 class result
170 {
171 char buf_[61];
172 unsigned char size_;
173
174 public:
175 /** Default constructor.
176
177 Constructs an empty result.
178 */
179 31 result() noexcept
180 31 : size_(0)
181 {
182 31 buf_[0] = '\0';
183 31 }
184
185 /** Return the hash as a string_view.
186 */
187 core::string_view
188 30 str() const noexcept
189 {
190 30 return core::string_view(buf_, size_);
191 }
192
193 /** Implicit conversion to string_view.
194 */
195 operator core::string_view() const noexcept
196 {
197 return str();
198 }
199
200 /** Return null-terminated C string.
201 */
202 char const*
203 1 c_str() const noexcept
204 {
205 1 return buf_;
206 }
207
208 /** Return pointer to data.
209 */
210 char const*
211 data() const noexcept
212 {
213 return buf_;
214 }
215
216 /** Return size in bytes (excludes null terminator).
217 */
218 std::size_t
219 7 size() const noexcept
220 {
221 7 return size_;
222 }
223
224 /** Check if result is empty.
225 */
226 bool
227 4 empty() const noexcept
228 {
229 4 return size_ == 0;
230 }
231
232 /** Check if result contains valid data.
233 */
234 explicit
235 2 operator bool() const noexcept
236 {
237 2 return size_ != 0;
238 }
239
240 private:
241 friend BOOST_HTTP_DECL result gen_salt(unsigned, version);
242 friend BOOST_HTTP_DECL result hash(core::string_view, unsigned, version);
243 friend BOOST_HTTP_DECL result hash(core::string_view, core::string_view, system::error_code&);
244
245 25 char* buf() noexcept { return buf_; }
246 25 void set_size(unsigned char n) noexcept
247 {
248 25 size_ = n;
249 25 buf_[n] = '\0';
250 25 }
251 };
252
253 //------------------------------------------------
254
255 /** Generate a random salt.
256
257 Creates a bcrypt salt string suitable for use with
258 the hash() function.
259
260 @par Preconditions
261 @code
262 rounds >= 4 && rounds <= 31
263 @endcode
264
265 @par Exception Safety
266 Strong guarantee.
267
268 @par Complexity
269 Constant.
270
271 @param rounds Cost factor. Each increment doubles the work.
272 Default is 10, which takes approximately 100ms on modern hardware.
273
274 @param ver Hash version to use.
275
276 @return A 29-character salt string.
277
278 @throws std::invalid_argument if rounds is out of range.
279 @throws system_error on RNG failure.
280 */
281 BOOST_HTTP_DECL
282 result
283 gen_salt(
284 unsigned rounds = 10,
285 version ver = version::v2b);
286
287 /** Hash a password with auto-generated salt.
288
289 Generates a random salt and hashes the password.
290
291 @par Preconditions
292 @code
293 rounds >= 4 && rounds <= 31
294 @endcode
295
296 @par Exception Safety
297 Strong guarantee.
298
299 @par Complexity
300 O(2^rounds).
301
302 @param password The password to hash. Only the first 72 bytes
303 are used (bcrypt limitation).
304
305 @param rounds Cost factor. Each increment doubles the work.
306
307 @param ver Hash version to use.
308
309 @return A 60-character hash string.
310
311 @throws std::invalid_argument if rounds is out of range.
312 @throws system_error on RNG failure.
313 */
314 BOOST_HTTP_DECL
315 result
316 hash(
317 core::string_view password,
318 unsigned rounds = 10,
319 version ver = version::v2b);
320
321 /** Hash a password using a provided salt.
322
323 Uses the given salt to hash the password. The salt should
324 be a string previously returned by gen_salt() or extracted
325 from a hash string.
326
327 @par Exception Safety
328 Strong guarantee.
329
330 @par Complexity
331 O(2^rounds).
332
333 @param password The password to hash.
334
335 @param salt The salt string (29 characters).
336
337 @param ec Set to bcrypt::error::invalid_salt if the salt
338 is malformed.
339
340 @return A 60-character hash string, or empty result on error.
341 */
342 BOOST_HTTP_DECL
343 result
344 hash(
345 core::string_view password,
346 core::string_view salt,
347 system::error_code& ec);
348
349 /** Compare a password against a hash.
350
351 Extracts the salt from the hash, re-hashes the password,
352 and compares the result.
353
354 @par Exception Safety
355 Strong guarantee.
356
357 @par Complexity
358 O(2^rounds).
359
360 @param password The plaintext password to check.
361
362 @param hash The hash string to compare against.
363
364 @param ec Set to bcrypt::error::invalid_hash if the hash
365 is malformed.
366
367 @return true if the password matches the hash, false if
368 it does not match OR if an error occurred. Always check
369 ec to distinguish between a mismatch and an error.
370 */
371 BOOST_HTTP_DECL
372 bool
373 compare(
374 core::string_view password,
375 core::string_view hash,
376 system::error_code& ec);
377
378 /** Extract the cost factor from a hash string.
379
380 @par Exception Safety
381 Strong guarantee.
382
383 @par Complexity
384 Constant.
385
386 @param hash The hash string to parse.
387
388 @param ec Set to bcrypt::error::invalid_hash if the hash
389 is malformed.
390
391 @return The cost factor (4-31) on success, or 0 if an
392 error occurred.
393 */
394 BOOST_HTTP_DECL
395 unsigned
396 get_rounds(
397 core::string_view hash,
398 system::error_code& ec);
399
400 namespace detail {
401
402 // bcrypt truncates passwords to 72 bytes
403 struct password_buf
404 {
405 char data_[72];
406 unsigned char size_;
407
408 14 explicit password_buf(
409 core::string_view s) noexcept
410 28 : size_(static_cast<unsigned char>(
411 14 (std::min)(s.size(), std::size_t{72})))
412 {
413 14 std::memcpy(data_, s.data(), size_);
414 14 }
415
416 14 operator core::string_view() const noexcept
417 {
418 14 return {data_, size_};
419 }
420 };
421
422 // bcrypt hashes are always 60 characters
423 struct hash_buf
424 {
425 char data_[61];
426 unsigned char size_;
427
428 9 explicit hash_buf(
429 core::string_view s) noexcept
430 18 : size_(static_cast<unsigned char>(
431 9 (std::min)(s.size(), std::size_t{60})))
432 {
433 9 std::memcpy(data_, s.data(), size_);
434 9 data_[size_] = '\0';
435 9 }
436
437 9 operator core::string_view() const noexcept
438 {
439 9 return {data_, size_};
440 }
441 };
442
443 } // detail
444
445 //------------------------------------------------
446
447 /** Hash a password, returning a lazy task.
448
449 Returns a @ref capy::task that wraps the synchronous
450 hash() call. The caller can co_await this task directly
451 or launch it on a specific executor via run_async().
452
453 @par Example
454 @code
455 // co_await in current context
456 bcrypt::result r = co_await bcrypt::hash_task("password", 12);
457
458 // or launch on a specific executor
459 run_async(my_executor)(bcrypt::hash_task("password", 12));
460 @endcode
461
462 @param password The password to hash.
463
464 @param rounds Cost factor. Each increment doubles the work.
465
466 @param ver Hash version to use.
467
468 @return A lazy task yielding `result`.
469
470 @throws std::invalid_argument if rounds is out of range.
471 @throws system_error on RNG failure.
472 */
473 inline
474 capy::task<result>
475
1/1
✓ Branch 1 taken 4 times.
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
1/1
✓ Branch 1 taken 6 times.
6 compare_task(
505 core::string_view password,
506 core::string_view hash_str)
507 {
508 detail::password_buf pw(password);
509 detail::hash_buf hs(hash_str);
510 system::error_code ec;
511 bool ok = compare(pw, hs, ec);
512 if(ec.failed())
513 http::detail::throw_system_error(ec);
514 co_return ok;
515 12 }
516
517 //------------------------------------------------
518
519 namespace detail {
520
521 struct hash_async_op
522 {
523 password_buf password_;
524 unsigned rounds_;
525 version ver_;
526 result result_;
527 std::exception_ptr ep_;
528
529 1 bool await_ready() const noexcept
530 {
531 1 return false;
532 }
533
534 1 void await_suspend(
535 capy::coro cont,
536 capy::executor_ref caller_ex,
537 std::stop_token)
538 {
539
1/1
✓ Branch 1 taken 1 time.
1 auto& pool = capy::get_system_context();
540 1 auto sys_ex = pool.get_executor();
541 1 capy::run_async(sys_ex,
542 1 [this, cont, caller_ex]
543 (result r) mutable
544 {
545 1 result_ = r;
546 1 caller_ex.dispatch(cont);
547 1 },
548 [this, cont, caller_ex]
549 (std::exception_ptr ep) mutable
550 {
551 ep_ = ep;
552 caller_ex.dispatch(cont);
553 }
554
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
1 )(hash_task(password_, rounds_, ver_));
555 1 }
556
557 1 result await_resume()
558 {
559
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 if(ep_)
560 std::rethrow_exception(ep_);
561 1 return result_;
562 }
563 };
564
565 struct compare_async_op
566 {
567 password_buf password_;
568 hash_buf hash_str_;
569 bool result_ = false;
570 std::exception_ptr ep_;
571
572 3 bool await_ready() const noexcept
573 {
574 3 return false;
575 }
576
577 3 void await_suspend(
578 capy::coro cont,
579 capy::executor_ref caller_ex,
580 std::stop_token)
581 {
582
1/1
✓ Branch 1 taken 3 times.
3 auto& pool = capy::get_system_context();
583 3 auto sys_ex = pool.get_executor();
584 3 capy::run_async(sys_ex,
585 2 [this, cont, caller_ex]
586 (bool ok) mutable
587 {
588 2 result_ = ok;
589 2 caller_ex.dispatch(cont);
590 2 },
591 1 [this, cont, caller_ex]
592 (std::exception_ptr ep) mutable
593 {
594 1 ep_ = ep;
595 1 caller_ex.dispatch(cont);
596 1 }
597
2/2
✓ Branch 3 taken 3 times.
✓ Branch 6 taken 3 times.
3 )(compare_task(password_, hash_str_));
598 3 }
599
600 3 bool await_resume()
601 {
602
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
3 if(ep_)
603 1 std::rethrow_exception(ep_);
604 2 return result_;
605 }
606 };
607
608 } // detail
609
610 /** Hash a password asynchronously on the system thread pool.
611
612 Returns an awaitable that offloads the CPU-intensive
613 bcrypt work to the system thread pool, then resumes
614 the caller on their original executor. Modeled after
615 Express.js: `await bcrypt.hash(password, 12)`.
616
617 @par Example
618 @code
619 bcrypt::result r = co_await bcrypt::hash_async("my_password", 12);
620 @endcode
621
622 @param password The password to hash.
623
624 @param rounds Cost factor. Each increment doubles the work.
625
626 @param ver Hash version to use.
627
628 @return An awaitable yielding `result`.
629
630 @throws std::invalid_argument if rounds is out of range.
631 @throws system_error on RNG failure.
632 */
633 inline
634 detail::hash_async_op
635 1 hash_async(
636 core::string_view password,
637 unsigned rounds = 10,
638 version ver = version::v2b)
639 {
640 1 return detail::hash_async_op{
641 detail::password_buf(password),
642 rounds,
643 ver,
644 {},
645 1 {}};
646 }
647
648 /** Compare a password against a hash asynchronously.
649
650 Returns an awaitable that offloads the CPU-intensive
651 bcrypt work to the system thread pool, then resumes
652 the caller on their original executor. Modeled after
653 Express.js: `await bcrypt.compare(password, hash)`.
654
655 @par Example
656 @code
657 bool ok = co_await bcrypt::compare_async("my_password", stored_hash);
658 @endcode
659
660 @param password The plaintext password to check.
661
662 @param hash_str The hash string to compare against.
663
664 @return An awaitable yielding `bool`.
665
666 @throws system_error if the hash is malformed.
667 */
668 inline
669 detail::compare_async_op
670 3 compare_async(
671 core::string_view password,
672 core::string_view hash_str)
673 {
674 3 return detail::compare_async_op{
675 detail::password_buf(password),
676 detail::hash_buf(hash_str),
677 false,
678 3 {}};
679 }
680
681 } // bcrypt
682 } // http
683 } // boost
684
685 #endif
686