include/boost/http/bcrypt.hpp

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