GCC Code Coverage Report


Directory: ./
File: libs/capy/src/bcrypt/hash.cpp
Date: 2025-12-30 20:31:36
Exec Total Coverage
Lines: 48 62 77.4%
Functions: 5 5 100.0%
Branches: 36 54 66.7%

Line Branch Exec Source
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
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (rounds < 4 || rounds > 31)
26 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
1/1
✓ Branch 1 taken 7 times.
7 detail::generate_salt_bytes(salt_bytes);
31
32 // Format salt string
33 7 result r;
34
1/1
✓ Branch 2 taken 7 times.
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
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (rounds < 4 || rounds > 31)
52 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
1/1
✓ Branch 1 taken 7 times.
7 detail::generate_salt_bytes(salt_bytes);
57
58 // Hash password
59 std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN];
60
1/1
✓ Branch 3 taken 7 times.
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
1/1
✓ Branch 2 taken 7 times.
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
3/3
✓ Branch 1 taken 7 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 5 times.
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
1/1
✓ Branch 3 taken 5 times.
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
1/1
✓ Branch 2 taken 5 times.
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
3/3
✓ Branch 1 taken 8 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 6 times.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
6 if (hash_str.size() != detail::BCRYPT_HASH_OUTPUT_LEN)
142 {
143 ec = make_error_code(error::invalid_hash);
144 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
1/1
✓ Branch 1 taken 6 times.
6 int decoded = detail::base64_decode(
150 stored_hash,
151 6 hash_str.data() + 29,
152 31);
153
154
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (decoded < 0)
155 {
156 ec = make_error_code(error::invalid_hash);
157 return false;
158 }
159
160 // Compute hash of provided password
161 std::uint8_t computed_hash[detail::BCRYPT_HASH_LEN];
162
1/1
✓ Branch 3 taken 6 times.
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
1/1
✓ Branch 1 taken 6 times.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
4 if (hash_str.size() < 7)
182 {
183 ec = make_error_code(error::invalid_hash);
184 return 0;
185 }
186
187 4 char const* s = hash_str.data();
188
189 // Check prefix
190
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
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
4/8
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 if ((s[2] != 'a' && s[2] != 'b' && s[2] != 'y') || s[3] != '$')
198 {
199 ec = make_error_code(error::invalid_hash);
200 return 0;
201 }
202
203 // Parse rounds
204
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 if (s[4] < '0' || s[4] > '9' || s[5] < '0' || s[5] > '9')
205 {
206 ec = make_error_code(error::invalid_hash);
207 return 0;
208 }
209
210 2 unsigned rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
211
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (rounds < 4 || rounds > 31)
212 {
213 ec = make_error_code(error::invalid_hash);
214 return 0;
215 }
216
217 2 return rounds;
218 }
219
220 } // bcrypt
221 } // capy
222 } // boost
223
224