1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
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)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/http
7  
// Official repository: https://github.com/cppalliance/http
8  
//
8  
//
9  

9  

10  
#include <boost/http/bcrypt.hpp>
10  
#include <boost/http/bcrypt.hpp>
11  
#include <boost/http/detail/except.hpp>
11  
#include <boost/http/detail/except.hpp>
12  
#include "base64.hpp"
12  
#include "base64.hpp"
13  
#include "crypt.hpp"
13  
#include "crypt.hpp"
14  

14  

15  
namespace boost {
15  
namespace boost {
16  
namespace http {
16  
namespace http {
17  
namespace bcrypt {
17  
namespace bcrypt {
18  

18  

19  
result
19  
result
20  
gen_salt(
20  
gen_salt(
21  
    unsigned rounds,
21  
    unsigned rounds,
22  
    version ver)
22  
    version ver)
23  
{
23  
{
24  
    // Validate preconditions
24  
    // Validate preconditions
25  
    if (rounds < 4 || rounds > 31)
25  
    if (rounds < 4 || rounds > 31)
26  
        http::detail::throw_invalid_argument("bcrypt rounds must be 4-31");
26  
        http::detail::throw_invalid_argument("bcrypt rounds must be 4-31");
27  

27  

28  
    // Generate random salt
28  
    // Generate random salt
29  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
29  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
30  
    detail::generate_salt_bytes(salt_bytes);
30  
    detail::generate_salt_bytes(salt_bytes);
31  

31  

32  
    // Format salt string
32  
    // Format salt string
33  
    result r;
33  
    result r;
34  
    std::size_t len = detail::format_salt(
34  
    std::size_t len = detail::format_salt(
35  
        r.buf(),
35  
        r.buf(),
36  
        salt_bytes,
36  
        salt_bytes,
37  
        rounds,
37  
        rounds,
38  
        ver);
38  
        ver);
39  

39  

40  
    r.set_size(static_cast<unsigned char>(len));
40  
    r.set_size(static_cast<unsigned char>(len));
41  
    return r;
41  
    return r;
42  
}
42  
}
43  

43  

44  
result
44  
result
45  
hash(
45  
hash(
46  
    core::string_view password,
46  
    core::string_view password,
47  
    unsigned rounds,
47  
    unsigned rounds,
48  
    version ver)
48  
    version ver)
49  
{
49  
{
50  
    // Validate preconditions
50  
    // Validate preconditions
51  
    if (rounds < 4 || rounds > 31)
51  
    if (rounds < 4 || rounds > 31)
52  
        http::detail::throw_invalid_argument("bcrypt rounds must be 4-31");
52  
        http::detail::throw_invalid_argument("bcrypt rounds must be 4-31");
53  

53  

54  
    // Generate random salt
54  
    // Generate random salt
55  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
55  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
56  
    detail::generate_salt_bytes(salt_bytes);
56  
    detail::generate_salt_bytes(salt_bytes);
57  

57  

58  
    // Hash password
58  
    // Hash password
59  
    std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
59  
    std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
60  
    detail::bcrypt_hash(
60  
    detail::bcrypt_hash(
61  
        password.data(),
61  
        password.data(),
62  
        password.size(),
62  
        password.size(),
63  
        salt_bytes,
63  
        salt_bytes,
64  
        rounds,
64  
        rounds,
65  
        hash_bytes);
65  
        hash_bytes);
66  

66  

67  
    // Format output
67  
    // Format output
68  
    result r;
68  
    result r;
69  
    std::size_t len = detail::format_hash(
69  
    std::size_t len = detail::format_hash(
70  
        r.buf(),
70  
        r.buf(),
71  
        salt_bytes,
71  
        salt_bytes,
72  
        hash_bytes,
72  
        hash_bytes,
73  
        rounds,
73  
        rounds,
74  
        ver);
74  
        ver);
75  

75  

76  
    r.set_size(static_cast<unsigned char>(len));
76  
    r.set_size(static_cast<unsigned char>(len));
77  
    return r;
77  
    return r;
78  
}
78  
}
79  

79  

80  
result
80  
result
81  
hash(
81  
hash(
82  
    core::string_view password,
82  
    core::string_view password,
83  
    core::string_view salt,
83  
    core::string_view salt,
84  
    system::error_code& ec)
84  
    system::error_code& ec)
85  
{
85  
{
86  
    ec = {};
86  
    ec = {};
87  

87  

88  
    // Parse salt
88  
    // Parse salt
89  
    version ver;
89  
    version ver;
90  
    unsigned rounds;
90  
    unsigned rounds;
91  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
91  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
92  

92  

93  
    if (!detail::parse_salt(salt, ver, rounds, salt_bytes))
93  
    if (!detail::parse_salt(salt, ver, rounds, salt_bytes))
94  
    {
94  
    {
95  
        ec = make_error_code(error::invalid_salt);
95  
        ec = make_error_code(error::invalid_salt);
96  
        return result{};
96  
        return result{};
97  
    }
97  
    }
98  

98  

99  
    // Hash password
99  
    // Hash password
100  
    std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
100  
    std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
101  
    detail::bcrypt_hash(
101  
    detail::bcrypt_hash(
102  
        password.data(),
102  
        password.data(),
103  
        password.size(),
103  
        password.size(),
104  
        salt_bytes,
104  
        salt_bytes,
105  
        rounds,
105  
        rounds,
106  
        hash_bytes);
106  
        hash_bytes);
107  

107  

108  
    // Format output
108  
    // Format output
109  
    result r;
109  
    result r;
110  
    std::size_t len = detail::format_hash(
110  
    std::size_t len = detail::format_hash(
111  
        r.buf(),
111  
        r.buf(),
112  
        salt_bytes,
112  
        salt_bytes,
113  
        hash_bytes,
113  
        hash_bytes,
114  
        rounds,
114  
        rounds,
115  
        ver);
115  
        ver);
116  

116  

117  
    r.set_size(static_cast<unsigned char>(len));
117  
    r.set_size(static_cast<unsigned char>(len));
118  
    return r;
118  
    return r;
119  
}
119  
}
120  

120  

121  
bool
121  
bool
122  
compare(
122  
compare(
123  
    core::string_view password,
123  
    core::string_view password,
124  
    core::string_view hash_str,
124  
    core::string_view hash_str,
125  
    system::error_code& ec)
125  
    system::error_code& ec)
126  
{
126  
{
127  
    ec = {};
127  
    ec = {};
128  

128  

129  
    // Parse hash to extract salt
129  
    // Parse hash to extract salt
130  
    version ver;
130  
    version ver;
131  
    unsigned rounds;
131  
    unsigned rounds;
132  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
132  
    std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN];
133  

133  

134  
    if (!detail::parse_salt(hash_str, ver, rounds, salt_bytes))
134  
    if (!detail::parse_salt(hash_str, ver, rounds, salt_bytes))
135  
    {
135  
    {
136  
        ec = make_error_code(error::invalid_hash);
136  
        ec = make_error_code(error::invalid_hash);
137  
        return false;
137  
        return false;
138  
    }
138  
    }
139  

139  

140  
    // Validate hash length
140  
    // Validate hash length
141  
    if (hash_str.size() != detail::BCRYPT_HASH_OUTPUT_LEN)
141  
    if (hash_str.size() != detail::BCRYPT_HASH_OUTPUT_LEN)
142  
    {
142  
    {
143  
        ec = make_error_code(error::invalid_hash);
143  
        ec = make_error_code(error::invalid_hash);
144  
        return false;
144  
        return false;
145  
    }
145  
    }
146  

146  

147  
    // Decode stored hash (31 base64 chars starting at position 29)
147  
    // Decode stored hash (31 base64 chars starting at position 29)
148  
    std::uint8_t stored_hash[detail::BCRYPT_HASH_LEN];
148  
    std::uint8_t stored_hash[detail::BCRYPT_HASH_LEN];
149  
    int decoded = detail::base64_decode(
149  
    int decoded = detail::base64_decode(
150  
        stored_hash,
150  
        stored_hash,
151  
        hash_str.data() + 29,
151  
        hash_str.data() + 29,
152  
        31);
152  
        31);
153  

153  

154  
    if (decoded < 0)
154  
    if (decoded < 0)
155  
    {
155  
    {
156  
        ec = make_error_code(error::invalid_hash);
156  
        ec = make_error_code(error::invalid_hash);
157  
        return false;
157  
        return false;
158  
    }
158  
    }
159  

159  

160  
    // Compute hash of provided password
160  
    // Compute hash of provided password
161  
    std::uint8_t computed_hash[detail::BCRYPT_HASH_LEN];
161  
    std::uint8_t computed_hash[detail::BCRYPT_HASH_LEN];
162  
    detail::bcrypt_hash(
162  
    detail::bcrypt_hash(
163  
        password.data(),
163  
        password.data(),
164  
        password.size(),
164  
        password.size(),
165  
        salt_bytes,
165  
        salt_bytes,
166  
        rounds,
166  
        rounds,
167  
        computed_hash);
167  
        computed_hash);
168  

168  

169  
    // Constant-time comparison (only first 23 bytes are used)
169  
    // Constant-time comparison (only first 23 bytes are used)
170  
    return detail::secure_compare(stored_hash, computed_hash, 23);
170  
    return detail::secure_compare(stored_hash, computed_hash, 23);
171  
}
171  
}
172  

172  

173  
unsigned
173  
unsigned
174  
get_rounds(
174  
get_rounds(
175  
    core::string_view hash_str,
175  
    core::string_view hash_str,
176  
    system::error_code& ec)
176  
    system::error_code& ec)
177  
{
177  
{
178  
    ec = {};
178  
    ec = {};
179  

179  

180  
    // Minimum length check
180  
    // Minimum length check
181  
    if (hash_str.size() < 7)
181  
    if (hash_str.size() < 7)
182  
    {
182  
    {
183  
        ec = make_error_code(error::invalid_hash);
183  
        ec = make_error_code(error::invalid_hash);
184  
        return 0;
184  
        return 0;
185  
    }
185  
    }
186  

186  

187  
    char const* s = hash_str.data();
187  
    char const* s = hash_str.data();
188  

188  

189  
    // Check prefix
189  
    // Check prefix
190  
    if (s[0] != '$' || s[1] != '2')
190  
    if (s[0] != '$' || s[1] != '2')
191  
    {
191  
    {
192  
        ec = make_error_code(error::invalid_hash);
192  
        ec = make_error_code(error::invalid_hash);
193  
        return 0;
193  
        return 0;
194  
    }
194  
    }
195  

195  

196  
    // Check version character
196  
    // Check version character
197  
    if ((s[2] != 'a' && s[2] != 'b' && s[2] != 'y') || s[3] != '$')
197  
    if ((s[2] != 'a' && s[2] != 'b' && s[2] != 'y') || s[3] != '$')
198  
    {
198  
    {
199  
        ec = make_error_code(error::invalid_hash);
199  
        ec = make_error_code(error::invalid_hash);
200  
        return 0;
200  
        return 0;
201  
    }
201  
    }
202  

202  

203  
    // Parse rounds
203  
    // Parse rounds
204  
    if (s[4] < '0' || s[4] > '9' || s[5] < '0' || s[5] > '9')
204  
    if (s[4] < '0' || s[4] > '9' || s[5] < '0' || s[5] > '9')
205  
    {
205  
    {
206  
        ec = make_error_code(error::invalid_hash);
206  
        ec = make_error_code(error::invalid_hash);
207  
        return 0;
207  
        return 0;
208  
    }
208  
    }
209  

209  

210  
    unsigned rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
210  
    unsigned rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
211  
    if (rounds < 4 || rounds > 31)
211  
    if (rounds < 4 || rounds > 31)
212  
    {
212  
    {
213  
        ec = make_error_code(error::invalid_hash);
213  
        ec = make_error_code(error::invalid_hash);
214  
        return 0;
214  
        return 0;
215  
    }
215  
    }
216  

216  

217  
    return rounds;
217  
    return rounds;
218  
}
218  
}
219  

219  

220  
} // bcrypt
220  
} // bcrypt
221  
} // http
221  
} // http
222  
} // boost
222  
} // boost