GCC Code Coverage Report


Directory: ./
File: libs/capy/src/bcrypt/crypt.cpp
Date: 2025-12-30 20:31:36
Exec Total Coverage
Lines: 64 74 86.5%
Functions: 7 7 100.0%
Branches: 30 48 62.5%

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 "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
2/3
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
19 switch (ver)
34 {
35 1 case version::v2a: return "$2a$";
36 18 case version::v2b: return "$2b$";
37 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
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 11 times.
15 if (salt_str.size() < 29)
82 4 return false;
83
84 11 char const* s = salt_str.data();
85
86 // Check prefix
87
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
11 if (s[0] != '$' || s[1] != '2')
88 return false;
89
90 // Parse version
91
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
11 if (s[2] == 'a' && s[3] == '$')
92 3 ver = version::v2a;
93
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 else if (s[2] == 'b' && s[3] == '$')
94 8 ver = version::v2b;
95 else if (s[2] == 'y' && s[3] == '$')
96 ver = version::v2b; // treat $2y$ as $2b$
97 else
98 return false;
99
100 // Parse rounds
101
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
11 if (s[4] < '0' || s[4] > '9')
102 return false;
103
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
11 if (s[5] < '0' || s[5] > '9')
104 return false;
105
106 11 rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
107
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
11 if (rounds < 4 || rounds > 31)
108 return false;
109
110
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (s[6] != '$')
111 return false;
112
113 // Decode salt (22 base64 chars -> 16 bytes)
114 11 int decoded = base64_decode(salt_bytes, s + 7, 22);
115
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (decoded != 16)
116 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
1/1
✓ Branch 1 taken 18 times.
18 blowfish_init(ctx);
142
143 // Expensive key setup (eksblowfish)
144
1/1
✓ Branch 1 taken 18 times.
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
2/2
✓ Branch 0 taken 368 times.
✓ Branch 1 taken 18 times.
386 for (std::uint64_t i = 0; i < iterations; ++i)
149 {
150
1/1
✓ Branch 1 taken 368 times.
368 blowfish_expand_key(ctx, key, key_len);
151
1/1
✓ Branch 1 taken 368 times.
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
2/2
✓ Branch 0 taken 1152 times.
✓ Branch 1 taken 18 times.
1170 for (int i = 0; i < 64; ++i)
159 {
160
1/1
✓ Branch 1 taken 1152 times.
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
2/2
✓ Branch 0 taken 138 times.
✓ Branch 1 taken 6 times.
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
208