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 "crypt.hpp"
11 : #include "base64.hpp"
12 : #include "blowfish.hpp"
13 : #include "random.hpp"
14 : #include <cstring>
15 : #include <algorithm>
16 :
17 : namespace boost {
18 : namespace capy {
19 : namespace bcrypt {
20 : namespace detail {
21 :
22 : namespace {
23 :
24 : // "OrpheanBeholderScryDoubt" - magic string for bcrypt
25 : constexpr std::uint8_t magic_text[24] = {
26 : 'O', 'r', 'p', 'h', 'e', 'a', 'n', 'B',
27 : 'e', 'h', 'o', 'l', 'd', 'e', 'r', 'S',
28 : 'c', 'r', 'y', 'D', 'o', 'u', 'b', 't'
29 : };
30 :
31 19 : char const* version_prefix(version ver)
32 : {
33 19 : switch (ver)
34 : {
35 1 : case version::v2a: return "$2a$";
36 18 : case version::v2b: return "$2b$";
37 0 : default: return "$2b$";
38 : }
39 : }
40 :
41 : } // namespace
42 :
43 14 : void generate_salt_bytes(std::uint8_t* salt)
44 : {
45 14 : fill_random(salt, BCRYPT_SALT_LEN);
46 14 : }
47 :
48 19 : std::size_t format_salt(
49 : char* output,
50 : std::uint8_t const* salt_bytes,
51 : unsigned rounds,
52 : version ver)
53 : {
54 19 : char* p = output;
55 :
56 : // Version prefix
57 19 : char const* prefix = version_prefix(ver);
58 19 : std::size_t prefix_len = 4;
59 19 : std::memcpy(p, prefix, prefix_len);
60 19 : p += prefix_len;
61 :
62 : // Rounds (2 digits, zero-padded)
63 19 : *p++ = static_cast<char>('0' + (rounds / 10));
64 19 : *p++ = static_cast<char>('0' + (rounds % 10));
65 19 : *p++ = '$';
66 :
67 : // Salt (22 base64 characters)
68 19 : std::size_t encoded = base64_encode(p, salt_bytes, BCRYPT_SALT_LEN);
69 19 : p += encoded;
70 :
71 19 : return static_cast<std::size_t>(p - output);
72 : }
73 :
74 15 : bool parse_salt(
75 : core::string_view salt_str,
76 : version& ver,
77 : unsigned& rounds,
78 : std::uint8_t* salt_bytes)
79 : {
80 : // Minimum: "$2a$XX$" + 22 chars = 29
81 15 : if (salt_str.size() < 29)
82 4 : return false;
83 :
84 11 : char const* s = salt_str.data();
85 :
86 : // Check prefix
87 11 : if (s[0] != '$' || s[1] != '2')
88 0 : return false;
89 :
90 : // Parse version
91 11 : if (s[2] == 'a' && s[3] == '$')
92 3 : ver = version::v2a;
93 8 : else if (s[2] == 'b' && s[3] == '$')
94 8 : ver = version::v2b;
95 0 : else if (s[2] == 'y' && s[3] == '$')
96 0 : ver = version::v2b; // treat $2y$ as $2b$
97 : else
98 0 : return false;
99 :
100 : // Parse rounds
101 11 : if (s[4] < '0' || s[4] > '9')
102 0 : return false;
103 11 : if (s[5] < '0' || s[5] > '9')
104 0 : return false;
105 :
106 11 : rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
107 11 : if (rounds < 4 || rounds > 31)
108 0 : return false;
109 :
110 11 : if (s[6] != '$')
111 0 : return false;
112 :
113 : // Decode salt (22 base64 chars -> 16 bytes)
114 11 : int decoded = base64_decode(salt_bytes, s + 7, 22);
115 11 : if (decoded != 16)
116 0 : return false;
117 :
118 11 : return true;
119 : }
120 :
121 18 : void bcrypt_hash(
122 : char const* password,
123 : std::size_t password_len,
124 : std::uint8_t const* salt,
125 : unsigned rounds,
126 : std::uint8_t* hash)
127 : {
128 : blowfish_ctx ctx;
129 :
130 : // Truncate password to 72 bytes (bcrypt limit)
131 : // Include null terminator in hash
132 18 : std::size_t key_len = std::min(password_len, std::size_t(72));
133 :
134 : // Create key with null terminator
135 : std::uint8_t key[73];
136 18 : std::memcpy(key, password, key_len);
137 18 : key[key_len] = 0;
138 18 : key_len++;
139 :
140 : // Initialize with default P and S boxes
141 18 : blowfish_init(ctx);
142 :
143 : // Expensive key setup (eksblowfish)
144 18 : blowfish_expand_key_salt(ctx, key, key_len, salt, BCRYPT_SALT_LEN);
145 :
146 : // 2^rounds iterations
147 18 : std::uint64_t iterations = 1ULL << rounds;
148 386 : for (std::uint64_t i = 0; i < iterations; ++i)
149 : {
150 368 : blowfish_expand_key(ctx, key, key_len);
151 368 : blowfish_expand_key(ctx, salt, BCRYPT_SALT_LEN);
152 : }
153 :
154 : // Encrypt magic text 64 times
155 : std::uint8_t ctext[24];
156 18 : std::memcpy(ctext, magic_text, 24);
157 :
158 1170 : for (int i = 0; i < 64; ++i)
159 : {
160 1152 : blowfish_encrypt_ecb(ctx, ctext, 24);
161 : }
162 :
163 : // Copy result (only 23 bytes are used in the final encoding)
164 18 : std::memcpy(hash, ctext, 24);
165 :
166 : // Clear sensitive data
167 18 : std::memset(&ctx, 0, sizeof(ctx));
168 18 : std::memset(key, 0, sizeof(key));
169 18 : }
170 :
171 12 : std::size_t format_hash(
172 : char* output,
173 : std::uint8_t const* salt_bytes,
174 : std::uint8_t const* hash_bytes,
175 : unsigned rounds,
176 : version ver)
177 : {
178 12 : char* p = output;
179 :
180 : // Format salt portion (29 chars)
181 12 : p += format_salt(p, salt_bytes, rounds, ver);
182 :
183 : // Encode hash (23 bytes -> 31 base64 chars)
184 : // Note: bcrypt only uses 23 of the 24 hash bytes
185 12 : p += base64_encode(p, hash_bytes, 23);
186 :
187 12 : return static_cast<std::size_t>(p - output);
188 : }
189 :
190 6 : bool secure_compare(
191 : std::uint8_t const* a,
192 : std::uint8_t const* b,
193 : std::size_t len)
194 : {
195 6 : volatile std::uint8_t result = 0;
196 144 : for (std::size_t i = 0; i < len; ++i)
197 : {
198 138 : result = static_cast<std::uint8_t>(result | (a[i] ^ b[i]));
199 : }
200 6 : return result == 0;
201 : }
202 :
203 : } // detail
204 : } // bcrypt
205 : } // capy
206 : } // boost
207 :
|