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 "crypt.hpp"
10  
#include "crypt.hpp"
11  
#include "base64.hpp"
11  
#include "base64.hpp"
12  
#include "blowfish.hpp"
12  
#include "blowfish.hpp"
13  
#include "random.hpp"
13  
#include "random.hpp"
14  
#include <cstring>
14  
#include <cstring>
15  
#include <algorithm>
15  
#include <algorithm>
16  

16  

17  
namespace boost {
17  
namespace boost {
18  
namespace http {
18  
namespace http {
19  
namespace bcrypt {
19  
namespace bcrypt {
20  
namespace detail {
20  
namespace detail {
21  

21  

22  
namespace {
22  
namespace {
23  

23  

24  
// "OrpheanBeholderScryDoubt" - magic string for bcrypt
24  
// "OrpheanBeholderScryDoubt" - magic string for bcrypt
25  
constexpr std::uint8_t magic_text[24] = {
25  
constexpr std::uint8_t magic_text[24] = {
26  
    'O', 'r', 'p', 'h', 'e', 'a', 'n', 'B',
26  
    'O', 'r', 'p', 'h', 'e', 'a', 'n', 'B',
27  
    'e', 'h', 'o', 'l', 'd', 'e', 'r', 'S',
27  
    'e', 'h', 'o', 'l', 'd', 'e', 'r', 'S',
28  
    'c', 'r', 'y', 'D', 'o', 'u', 'b', 't'
28  
    'c', 'r', 'y', 'D', 'o', 'u', 'b', 't'
29  
};
29  
};
30  

30  

31  
char const* version_prefix(version ver)
31  
char const* version_prefix(version ver)
32  
{
32  
{
33  
    switch (ver)
33  
    switch (ver)
34  
    {
34  
    {
35  
    case version::v2a: return "$2a$";
35  
    case version::v2a: return "$2a$";
36  
    case version::v2b: return "$2b$";
36  
    case version::v2b: return "$2b$";
37  
    default: return "$2b$";
37  
    default: return "$2b$";
38  
    }
38  
    }
39  
}
39  
}
40  

40  

41  
} // namespace
41  
} // namespace
42  

42  

43  
void generate_salt_bytes(std::uint8_t* salt)
43  
void generate_salt_bytes(std::uint8_t* salt)
44  
{
44  
{
45  
    fill_random(salt, BCRYPT_SALT_LEN);
45  
    fill_random(salt, BCRYPT_SALT_LEN);
46  
}
46  
}
47  

47  

48  
std::size_t format_salt(
48  
std::size_t format_salt(
49  
    char* output,
49  
    char* output,
50  
    std::uint8_t const* salt_bytes,
50  
    std::uint8_t const* salt_bytes,
51  
    unsigned rounds,
51  
    unsigned rounds,
52  
    version ver)
52  
    version ver)
53  
{
53  
{
54  
    char* p = output;
54  
    char* p = output;
55  

55  

56  
    // Version prefix
56  
    // Version prefix
57  
    char const* prefix = version_prefix(ver);
57  
    char const* prefix = version_prefix(ver);
58  
    std::size_t prefix_len = 4;
58  
    std::size_t prefix_len = 4;
59  
    std::memcpy(p, prefix, prefix_len);
59  
    std::memcpy(p, prefix, prefix_len);
60  
    p += prefix_len;
60  
    p += prefix_len;
61  

61  

62  
    // Rounds (2 digits, zero-padded)
62  
    // Rounds (2 digits, zero-padded)
63  
    *p++ = static_cast<char>('0' + (rounds / 10));
63  
    *p++ = static_cast<char>('0' + (rounds / 10));
64  
    *p++ = static_cast<char>('0' + (rounds % 10));
64  
    *p++ = static_cast<char>('0' + (rounds % 10));
65  
    *p++ = '$';
65  
    *p++ = '$';
66  

66  

67  
    // Salt (22 base64 characters)
67  
    // Salt (22 base64 characters)
68  
    std::size_t encoded = base64_encode(p, salt_bytes, BCRYPT_SALT_LEN);
68  
    std::size_t encoded = base64_encode(p, salt_bytes, BCRYPT_SALT_LEN);
69  
    p += encoded;
69  
    p += encoded;
70  

70  

71  
    return static_cast<std::size_t>(p - output);
71  
    return static_cast<std::size_t>(p - output);
72  
}
72  
}
73  

73  

74  
bool parse_salt(
74  
bool parse_salt(
75  
    core::string_view salt_str,
75  
    core::string_view salt_str,
76  
    version& ver,
76  
    version& ver,
77  
    unsigned& rounds,
77  
    unsigned& rounds,
78  
    std::uint8_t* salt_bytes)
78  
    std::uint8_t* salt_bytes)
79  
{
79  
{
80  
    // Minimum: "$2a$XX$" + 22 chars = 29
80  
    // Minimum: "$2a$XX$" + 22 chars = 29
81  
    if (salt_str.size() < 29)
81  
    if (salt_str.size() < 29)
82  
        return false;
82  
        return false;
83  

83  

84  
    char const* s = salt_str.data();
84  
    char const* s = salt_str.data();
85  

85  

86  
    // Check prefix
86  
    // Check prefix
87  
    if (s[0] != '$' || s[1] != '2')
87  
    if (s[0] != '$' || s[1] != '2')
88  
        return false;
88  
        return false;
89  

89  

90  
    // Parse version
90  
    // Parse version
91  
    if (s[2] == 'a' && s[3] == '$')
91  
    if (s[2] == 'a' && s[3] == '$')
92  
        ver = version::v2a;
92  
        ver = version::v2a;
93  
    else if (s[2] == 'b' && s[3] == '$')
93  
    else if (s[2] == 'b' && s[3] == '$')
94  
        ver = version::v2b;
94  
        ver = version::v2b;
95  
    else if (s[2] == 'y' && s[3] == '$')
95  
    else if (s[2] == 'y' && s[3] == '$')
96  
        ver = version::v2b;  // treat $2y$ as $2b$
96  
        ver = version::v2b;  // treat $2y$ as $2b$
97  
    else
97  
    else
98  
        return false;
98  
        return false;
99  

99  

100  
    // Parse rounds
100  
    // Parse rounds
101  
    if (s[4] < '0' || s[4] > '9')
101  
    if (s[4] < '0' || s[4] > '9')
102  
        return false;
102  
        return false;
103  
    if (s[5] < '0' || s[5] > '9')
103  
    if (s[5] < '0' || s[5] > '9')
104  
        return false;
104  
        return false;
105  

105  

106  
    rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
106  
    rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
107  
    if (rounds < 4 || rounds > 31)
107  
    if (rounds < 4 || rounds > 31)
108  
        return false;
108  
        return false;
109  

109  

110  
    if (s[6] != '$')
110  
    if (s[6] != '$')
111  
        return false;
111  
        return false;
112  

112  

113  
    // Decode salt (22 base64 chars -> 16 bytes)
113  
    // Decode salt (22 base64 chars -> 16 bytes)
114  
    int decoded = base64_decode(salt_bytes, s + 7, 22);
114  
    int decoded = base64_decode(salt_bytes, s + 7, 22);
115  
    if (decoded != 16)
115  
    if (decoded != 16)
116  
        return false;
116  
        return false;
117  

117  

118  
    return true;
118  
    return true;
119  
}
119  
}
120  

120  

121  
void bcrypt_hash(
121  
void bcrypt_hash(
122  
    char const* password,
122  
    char const* password,
123  
    std::size_t password_len,
123  
    std::size_t password_len,
124  
    std::uint8_t const* salt,
124  
    std::uint8_t const* salt,
125  
    unsigned rounds,
125  
    unsigned rounds,
126  
    std::uint8_t* hash)
126  
    std::uint8_t* hash)
127  
{
127  
{
128  
    blowfish_ctx ctx;
128  
    blowfish_ctx ctx;
129  

129  

130  
    // Truncate password to 72 bytes (bcrypt limit)
130  
    // Truncate password to 72 bytes (bcrypt limit)
131  
    // Include null terminator in hash
131  
    // Include null terminator in hash
132  
    std::size_t key_len = std::min(password_len, std::size_t(72));
132  
    std::size_t key_len = std::min(password_len, std::size_t(72));
133  

133  

134  
    // Create key with null terminator
134  
    // Create key with null terminator
135  
    std::uint8_t key[73];
135  
    std::uint8_t key[73];
136  
    std::memcpy(key, password, key_len);
136  
    std::memcpy(key, password, key_len);
137  
    key[key_len] = 0;
137  
    key[key_len] = 0;
138  
    key_len++;
138  
    key_len++;
139  

139  

140  
    // Initialize with default P and S boxes
140  
    // Initialize with default P and S boxes
141  
    blowfish_init(ctx);
141  
    blowfish_init(ctx);
142  

142  

143  
    // Expensive key setup (eksblowfish)
143  
    // Expensive key setup (eksblowfish)
144  
    blowfish_expand_key_salt(ctx, key, key_len, salt, BCRYPT_SALT_LEN);
144  
    blowfish_expand_key_salt(ctx, key, key_len, salt, BCRYPT_SALT_LEN);
145  

145  

146  
    // 2^rounds iterations
146  
    // 2^rounds iterations
147  
    std::uint64_t iterations = 1ULL << rounds;
147  
    std::uint64_t iterations = 1ULL << rounds;
148  
    for (std::uint64_t i = 0; i < iterations; ++i)
148  
    for (std::uint64_t i = 0; i < iterations; ++i)
149  
    {
149  
    {
150  
        blowfish_expand_key(ctx, key, key_len);
150  
        blowfish_expand_key(ctx, key, key_len);
151  
        blowfish_expand_key(ctx, salt, BCRYPT_SALT_LEN);
151  
        blowfish_expand_key(ctx, salt, BCRYPT_SALT_LEN);
152  
    }
152  
    }
153  

153  

154  
    // Encrypt magic text 64 times
154  
    // Encrypt magic text 64 times
155  
    std::uint8_t ctext[24];
155  
    std::uint8_t ctext[24];
156  
    std::memcpy(ctext, magic_text, 24);
156  
    std::memcpy(ctext, magic_text, 24);
157  

157  

158  
    for (int i = 0; i < 64; ++i)
158  
    for (int i = 0; i < 64; ++i)
159  
    {
159  
    {
160  
        blowfish_encrypt_ecb(ctx, ctext, 24);
160  
        blowfish_encrypt_ecb(ctx, ctext, 24);
161  
    }
161  
    }
162  

162  

163  
    // Copy result (only 23 bytes are used in the final encoding)
163  
    // Copy result (only 23 bytes are used in the final encoding)
164  
    std::memcpy(hash, ctext, 24);
164  
    std::memcpy(hash, ctext, 24);
165  

165  

166  
    // Clear sensitive data
166  
    // Clear sensitive data
167  
    std::memset(&ctx, 0, sizeof(ctx));
167  
    std::memset(&ctx, 0, sizeof(ctx));
168  
    std::memset(key, 0, sizeof(key));
168  
    std::memset(key, 0, sizeof(key));
169  
}
169  
}
170  

170  

171  
std::size_t format_hash(
171  
std::size_t format_hash(
172  
    char* output,
172  
    char* output,
173  
    std::uint8_t const* salt_bytes,
173  
    std::uint8_t const* salt_bytes,
174  
    std::uint8_t const* hash_bytes,
174  
    std::uint8_t const* hash_bytes,
175  
    unsigned rounds,
175  
    unsigned rounds,
176  
    version ver)
176  
    version ver)
177  
{
177  
{
178  
    char* p = output;
178  
    char* p = output;
179  

179  

180  
    // Format salt portion (29 chars)
180  
    // Format salt portion (29 chars)
181  
    p += format_salt(p, salt_bytes, rounds, ver);
181  
    p += format_salt(p, salt_bytes, rounds, ver);
182  

182  

183  
    // Encode hash (23 bytes -> 31 base64 chars)
183  
    // Encode hash (23 bytes -> 31 base64 chars)
184  
    // Note: bcrypt only uses 23 of the 24 hash bytes
184  
    // Note: bcrypt only uses 23 of the 24 hash bytes
185  
    p += base64_encode(p, hash_bytes, 23);
185  
    p += base64_encode(p, hash_bytes, 23);
186  

186  

187  
    return static_cast<std::size_t>(p - output);
187  
    return static_cast<std::size_t>(p - output);
188  
}
188  
}
189  

189  

190  
bool secure_compare(
190  
bool secure_compare(
191  
    std::uint8_t const* a,
191  
    std::uint8_t const* a,
192  
    std::uint8_t const* b,
192  
    std::uint8_t const* b,
193  
    std::size_t len)
193  
    std::size_t len)
194  
{
194  
{
195  
    volatile std::uint8_t result = 0;
195  
    volatile std::uint8_t result = 0;
196  
    for (std::size_t i = 0; i < len; ++i)
196  
    for (std::size_t i = 0; i < len; ++i)
197  
    {
197  
    {
198  
        result = static_cast<std::uint8_t>(result | (a[i] ^ b[i]));
198  
        result = static_cast<std::uint8_t>(result | (a[i] ^ b[i]));
199  
    }
199  
    }
200  
    return result == 0;
200  
    return result == 0;
201  
}
201  
}
202  

202  

203  
} // detail
203  
} // detail
204  
} // bcrypt
204  
} // bcrypt
205  
} // http
205  
} // http
206  
} // boost
206  
} // boost