LCOV - code coverage report
Current view: top level - libs/capy/src/bcrypt - crypt.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 86.5 % 74 64
Test Date: 2025-12-31 15:26:29 Functions: 100.0 % 7 7

            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              : 
        

Generated by: LCOV version 2.1