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/capy
8 : //
9 :
10 : #include <boost/capy/bcrypt/hash.hpp>
11 : #include <boost/capy/detail/except.hpp>
12 : #include "base64.hpp"
13 : #include "crypt.hpp"
14 :
15 : namespace boost {
16 : namespace capy {
17 : namespace bcrypt {
18 :
19 : result
20 7 : gen_salt(
21 : unsigned rounds,
22 : version ver)
23 : {
24 : // Validate preconditions
25 7 : if (rounds < 4 || rounds > 31)
26 0 : capy::detail::throw_invalid_argument("bcrypt rounds must be 4-31");
27 :
28 : // Generate random salt
29 : std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
30 7 : detail::generate_salt_bytes(salt_bytes);
31 :
32 : // Format salt string
33 7 : result r;
34 7 : std::size_t len = detail::format_salt(
35 : r.buf(),
36 : salt_bytes,
37 : rounds,
38 : ver);
39 :
40 7 : r.set_size(static_cast<unsigned char>(len));
41 14 : return r;
42 : }
43 :
44 : result
45 7 : hash(
46 : core::string_view password,
47 : unsigned rounds,
48 : version ver)
49 : {
50 : // Validate preconditions
51 7 : if (rounds < 4 || rounds > 31)
52 0 : capy::detail::throw_invalid_argument("bcrypt rounds must be 4-31");
53 :
54 : // Generate random salt
55 : std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
56 7 : detail::generate_salt_bytes(salt_bytes);
57 :
58 : // Hash password
59 : std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
60 7 : detail::bcrypt_hash(
61 : password.data(),
62 : password.size(),
63 : salt_bytes,
64 : rounds,
65 : hash_bytes);
66 :
67 : // Format output
68 7 : result r;
69 7 : std::size_t len = detail::format_hash(
70 : r.buf(),
71 : salt_bytes,
72 : hash_bytes,
73 : rounds,
74 : ver);
75 :
76 7 : r.set_size(static_cast<unsigned char>(len));
77 14 : return r;
78 : }
79 :
80 : result
81 7 : hash(
82 : core::string_view password,
83 : core::string_view salt,
84 : system::error_code& ec)
85 : {
86 7 : ec = {};
87 :
88 : // Parse salt
89 : version ver;
90 : unsigned rounds;
91 : std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
92 :
93 7 : if (!detail::parse_salt(salt, ver, rounds, salt_bytes))
94 : {
95 2 : ec = make_error_code(error::invalid_salt);
96 2 : return result{};
97 : }
98 :
99 : // Hash password
100 : std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
101 5 : detail::bcrypt_hash(
102 : password.data(),
103 : password.size(),
104 : salt_bytes,
105 : rounds,
106 : hash_bytes);
107 :
108 : // Format output
109 5 : result r;
110 5 : std::size_t len = detail::format_hash(
111 : r.buf(),
112 : salt_bytes,
113 : hash_bytes,
114 : rounds,
115 : ver);
116 :
117 5 : r.set_size(static_cast<unsigned char>(len));
118 5 : return r;
119 : }
120 :
121 : bool
122 8 : compare(
123 : core::string_view password,
124 : core::string_view hash_str,
125 : system::error_code& ec)
126 : {
127 8 : ec = {};
128 :
129 : // Parse hash to extract salt
130 : version ver;
131 : unsigned rounds;
132 : std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
133 :
134 8 : if (!detail::parse_salt(hash_str, ver, rounds, salt_bytes))
135 : {
136 2 : ec = make_error_code(error::invalid_hash);
137 2 : return false;
138 : }
139 :
140 : // Validate hash length
141 6 : if (hash_str.size() != detail::BCRYPT_HASH_OUTPUT_LEN)
142 : {
143 0 : ec = make_error_code(error::invalid_hash);
144 0 : return false;
145 : }
146 :
147 : // Decode stored hash (31 base64 chars starting at position 29)
148 : std::uint8_t stored_hash[detail::BCRYPT_HASH_LEN];
149 6 : int decoded = detail::base64_decode(
150 : stored_hash,
151 6 : hash_str.data() + 29,
152 : 31);
153 :
154 6 : if (decoded < 0)
155 : {
156 0 : ec = make_error_code(error::invalid_hash);
157 0 : return false;
158 : }
159 :
160 : // Compute hash of provided password
161 : std::uint8_t computed_hash[detail::BCRYPT_HASH_LEN];
162 6 : detail::bcrypt_hash(
163 : password.data(),
164 : password.size(),
165 : salt_bytes,
166 : rounds,
167 : computed_hash);
168 :
169 : // Constant-time comparison (only first 23 bytes are used)
170 6 : return detail::secure_compare(stored_hash, computed_hash, 23);
171 : }
172 :
173 : unsigned
174 4 : get_rounds(
175 : core::string_view hash_str,
176 : system::error_code& ec)
177 : {
178 4 : ec = {};
179 :
180 : // Minimum length check
181 4 : if (hash_str.size() < 7)
182 : {
183 0 : ec = make_error_code(error::invalid_hash);
184 0 : return 0;
185 : }
186 :
187 4 : char const* s = hash_str.data();
188 :
189 : // Check prefix
190 4 : if (s[0] != '$' || s[1] != '2')
191 : {
192 2 : ec = make_error_code(error::invalid_hash);
193 2 : return 0;
194 : }
195 :
196 : // Check version character
197 2 : if ((s[2] != 'a' && s[2] != 'b' && s[2] != 'y') || s[3] != '$')
198 : {
199 0 : ec = make_error_code(error::invalid_hash);
200 0 : return 0;
201 : }
202 :
203 : // Parse rounds
204 2 : if (s[4] < '0' || s[4] > '9' || s[5] < '0' || s[5] > '9')
205 : {
206 0 : ec = make_error_code(error::invalid_hash);
207 0 : return 0;
208 : }
209 :
210 2 : unsigned rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
211 2 : if (rounds < 4 || rounds > 31)
212 : {
213 0 : ec = make_error_code(error::invalid_hash);
214 0 : return 0;
215 : }
216 :
217 2 : return rounds;
218 : }
219 :
220 : } // bcrypt
221 : } // capy
222 : } // boost
223 :
|