LCOV - code coverage report
Current view: top level - src/bcrypt - crypt.cpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 86.5 % 74 64 10
Test Date: 2026-06-13 19:44:58 Functions: 100.0 % 7 7

           TLA  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/http
       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 http {
      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 HIT          25 : char const* version_prefix(version ver)
      32                 : {
      33              25 :     switch (ver)
      34                 :     {
      35               1 :     case version::v2a: return "$2a$";
      36              24 :     case version::v2b: return "$2b$";
      37 MIS           0 :     default: return "$2b$";
      38                 :     }
      39                 : }
      40                 : 
      41                 : } // namespace
      42                 : 
      43 HIT          20 : void generate_salt_bytes(std::uint8_t* salt)
      44                 : {
      45              20 :     fill_random(salt, BCRYPT_SALT_LEN);
      46              20 : }
      47                 : 
      48              25 : std::size_t format_salt(
      49                 :     char* output,
      50                 :     std::uint8_t const* salt_bytes,
      51                 :     unsigned rounds,
      52                 :     version ver)
      53                 : {
      54              25 :     char* p = output;
      55                 : 
      56                 :     // Version prefix
      57              25 :     char const* prefix = version_prefix(ver);
      58              25 :     std::size_t prefix_len = 4;
      59              25 :     std::memcpy(p, prefix, prefix_len);
      60              25 :     p += prefix_len;
      61                 : 
      62                 :     // Rounds (2 digits, zero-padded)
      63              25 :     *p++ = static_cast<char>('0' + (rounds / 10));
      64              25 :     *p++ = static_cast<char>('0' + (rounds % 10));
      65              25 :     *p++ = '$';
      66                 : 
      67                 :     // Salt (22 base64 characters)
      68              25 :     std::size_t encoded = base64_encode(p, salt_bytes, BCRYPT_SALT_LEN);
      69              25 :     p += encoded;
      70                 : 
      71              25 :     return static_cast<std::size_t>(p - output);
      72                 : }
      73                 : 
      74              21 : 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              21 :     if (salt_str.size() < 29)
      82               6 :         return false;
      83                 : 
      84              15 :     char const* s = salt_str.data();
      85                 : 
      86                 :     // Check prefix
      87              15 :     if (s[0] != '$' || s[1] != '2')
      88 MIS           0 :         return false;
      89                 : 
      90                 :     // Parse version
      91 HIT          15 :     if (s[2] == 'a' && s[3] == '$')
      92               3 :         ver = version::v2a;
      93              12 :     else if (s[2] == 'b' && s[3] == '$')
      94              12 :         ver = version::v2b;
      95 MIS           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 HIT          15 :     if (s[4] < '0' || s[4] > '9')
     102 MIS           0 :         return false;
     103 HIT          15 :     if (s[5] < '0' || s[5] > '9')
     104 MIS           0 :         return false;
     105                 : 
     106 HIT          15 :     rounds = static_cast<unsigned>((s[4] - '0') * 10 + (s[5] - '0'));
     107              15 :     if (rounds < 4 || rounds > 31)
     108 MIS           0 :         return false;
     109                 : 
     110 HIT          15 :     if (s[6] != '$')
     111 MIS           0 :         return false;
     112                 : 
     113                 :     // Decode salt (22 base64 chars -> 16 bytes)
     114 HIT          15 :     int decoded = base64_decode(salt_bytes, s + 7, 22);
     115              15 :     if (decoded != 16)
     116 MIS           0 :         return false;
     117                 : 
     118 HIT          15 :     return true;
     119                 : }
     120                 : 
     121              28 : 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              28 :     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              28 :     std::memcpy(key, password, key_len);
     137              28 :     key[key_len] = 0;
     138              28 :     key_len++;
     139                 : 
     140                 :     // Initialize with default P and S boxes
     141              28 :     blowfish_init(ctx);
     142                 : 
     143                 :     // Expensive key setup (eksblowfish)
     144              28 :     blowfish_expand_key_salt(ctx, key, key_len, salt, BCRYPT_SALT_LEN);
     145                 : 
     146                 :     // 2^rounds iterations
     147              28 :     std::uint64_t iterations = 1ULL << rounds;
     148             556 :     for (std::uint64_t i = 0; i < iterations; ++i)
     149                 :     {
     150             528 :         blowfish_expand_key(ctx, key, key_len);
     151             528 :         blowfish_expand_key(ctx, salt, BCRYPT_SALT_LEN);
     152                 :     }
     153                 : 
     154                 :     // Encrypt magic text 64 times
     155                 :     std::uint8_t ctext[24];
     156              28 :     std::memcpy(ctext, magic_text, 24);
     157                 : 
     158            1820 :     for (int i = 0; i < 64; ++i)
     159                 :     {
     160            1792 :         blowfish_encrypt_ecb(ctx, ctext, 24);
     161                 :     }
     162                 : 
     163                 :     // Copy result (only 23 bytes are used in the final encoding)
     164              28 :     std::memcpy(hash, ctext, 24);
     165                 : 
     166                 :     // Clear sensitive data
     167              28 :     std::memset(&ctx, 0, sizeof(ctx));
     168              28 :     std::memset(key, 0, sizeof(key));
     169              28 : }
     170                 : 
     171              18 : 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              18 :     char* p = output;
     179                 : 
     180                 :     // Format salt portion (29 chars)
     181              18 :     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              18 :     p += base64_encode(p, hash_bytes, 23);
     186                 : 
     187              18 :     return static_cast<std::size_t>(p - output);
     188                 : }
     189                 : 
     190              10 : bool secure_compare(
     191                 :     std::uint8_t const* a,
     192                 :     std::uint8_t const* b,
     193                 :     std::size_t len)
     194                 : {
     195              10 :     volatile std::uint8_t result = 0;
     196             240 :     for (std::size_t i = 0; i < len; ++i)
     197                 :     {
     198             230 :         result = static_cast<std::uint8_t>(result | (a[i] ^ b[i]));
     199                 :     }
     200              10 :     return result == 0;
     201                 : }
     202                 : 
     203                 : } // detail
     204                 : } // bcrypt
     205                 : } // http
     206                 : } // boost
        

Generated by: LCOV version 2.3