diff --git a/CMakeLists.txt b/CMakeLists.txt index 6da6d84dd..d92fbad67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,7 +121,7 @@ endif() if(SOURCEMETA_CORE_CRYPTO) if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) - find_package(OpenSSL REQUIRED) + find_package(OpenSSL 3.0 REQUIRED) endif() add_subdirectory(src/core/crypto) endif() diff --git a/config.cmake.in b/config.cmake.in index 31e34d56f..8d5e1f22c 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -64,7 +64,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") elseif(component STREQUAL "crypto") if(@SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL@) - find_dependency(OpenSSL) + find_dependency(OpenSSL 3.0) endif() include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") elseif(component STREQUAL "regex") diff --git a/src/core/crypto/CMakeLists.txt b/src/core/crypto/CMakeLists.txt index e3ce000ab..c4b17052e 100644 --- a/src/core/crypto/CMakeLists.txt +++ b/src/core/crypto/CMakeLists.txt @@ -7,6 +7,10 @@ if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) target_compile_definitions(sourcemeta_core_crypto PRIVATE SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) +elseif(APPLE) + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Security") +elseif(WIN32) + target_link_libraries(sourcemeta_core_crypto PRIVATE bcrypt) endif() if(SOURCEMETA_CORE_INSTALL) diff --git a/src/core/crypto/crypto_sha1.cc b/src/core/crypto/crypto_sha1.cc index 33dccfadf..257104abd 100644 --- a/src/core/crypto/crypto_sha1.cc +++ b/src/core/crypto/crypto_sha1.cc @@ -3,9 +3,24 @@ #include // std::array #include // std::uint32_t, std::uint64_t -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL +#if defined(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) #include // EVP_MD_CTX_new, EVP_DigestInit_ex, EVP_sha1, EVP_DigestUpdate, EVP_DigestFinal_ex, EVP_MD_CTX_free #include // std::runtime_error +#elif defined(__APPLE__) +#include // CC_SHA1*, CC_LONG + +#include // std::size_t +#include // std::numeric_limits +#elif defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::size_t +#include // std::numeric_limits +#include // std::runtime_error #else #include // std::memcpy #endif @@ -16,7 +31,7 @@ constexpr std::array HEX_DIGITS{{'0', '1', '2', '3', '4', '5', '6', 'e', 'f', '\0'}}; } // namespace -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL +#if defined(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) namespace sourcemeta::core { @@ -51,9 +66,105 @@ auto sha1(const std::string_view input) -> std::string { return result; } -auto sha1(const std::string_view input, std::ostream &output) -> void { - const auto result = sha1(input); - output.write(result.data(), static_cast(result.size())); +} // namespace sourcemeta::core + +#elif defined(__APPLE__) + +namespace sourcemeta::core { + +auto sha1(const std::string_view input) -> std::string { + // The platform marks its SHA-1 interfaces as deprecated because the + // algorithm is cryptographically broken, but this module keeps exposing + // SHA-1 for non-security use cases +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CC_SHA1_CTX context; + CC_SHA1_Init(&context); + + // The platform update interface takes a 32-bit length, so larger + // inputs must be fed in chunks + const auto *remaining_data{input.data()}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + while (remaining_size > 0) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + CC_SHA1_Update(&context, remaining_data, static_cast(chunk_size)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + CC_SHA1_Final(digest.data(), &context); +#pragma clang diagnostic pop + + std::string result; + result.reserve(40); + for (std::uint64_t index = 0; index < 20u; ++index) { + result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); + result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); + } + + return result; +} + +} // namespace sourcemeta::core + +#elif defined(_WIN32) + +namespace sourcemeta::core { + +auto sha1(const std::string_view input) -> std::string { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0))) { + throw std::runtime_error("Could not open the CNG SHA-1 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG SHA-1 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(input.data()))}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG SHA-1 digest"); + } + + std::string result; + result.reserve(40); + for (std::uint64_t index = 0; index < 20u; ++index) { + result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); + result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); + } + + return result; } } // namespace sourcemeta::core @@ -213,11 +324,15 @@ auto sha1(const std::string_view input) -> std::string { return result; } +} // namespace sourcemeta::core + +#endif + +namespace sourcemeta::core { + auto sha1(const std::string_view input, std::ostream &output) -> void { const auto result = sha1(input); output.write(result.data(), static_cast(result.size())); } } // namespace sourcemeta::core - -#endif diff --git a/src/core/crypto/crypto_sha256.cc b/src/core/crypto/crypto_sha256.cc index 0d830da64..69241fe47 100644 --- a/src/core/crypto/crypto_sha256.cc +++ b/src/core/crypto/crypto_sha256.cc @@ -3,9 +3,24 @@ #include // std::array #include // std::uint32_t, std::uint64_t -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL +#if defined(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) #include // EVP_MD_CTX_new, EVP_DigestInit_ex, EVP_sha256, EVP_DigestUpdate, EVP_DigestFinal_ex, EVP_MD_CTX_free #include // std::runtime_error +#elif defined(__APPLE__) +#include // CC_SHA256*, CC_LONG + +#include // std::size_t +#include // std::numeric_limits +#elif defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::size_t +#include // std::numeric_limits +#include // std::runtime_error #else #include // std::memcpy #endif @@ -16,7 +31,7 @@ constexpr std::array HEX_DIGITS{{'0', '1', '2', '3', '4', '5', '6', 'e', 'f', '\0'}}; } // namespace -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL +#if defined(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) namespace sourcemeta::core { @@ -51,9 +66,100 @@ auto sha256(const std::string_view input) -> std::string { return result; } -auto sha256(const std::string_view input, std::ostream &output) -> void { - const auto result = sha256(input); - output.write(result.data(), static_cast(result.size())); +} // namespace sourcemeta::core + +#elif defined(__APPLE__) + +namespace sourcemeta::core { + +auto sha256(const std::string_view input) -> std::string { + CC_SHA256_CTX context; + CC_SHA256_Init(&context); + + // The platform update interface takes a 32-bit length, so larger + // inputs must be fed in chunks + const auto *remaining_data{input.data()}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + while (remaining_size > 0) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + CC_SHA256_Update(&context, remaining_data, + static_cast(chunk_size)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + CC_SHA256_Final(digest.data(), &context); + + std::string result; + result.reserve(64); + for (std::uint64_t index = 0; index < 32u; ++index) { + result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); + result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); + } + + return result; +} + +} // namespace sourcemeta::core + +#elif defined(_WIN32) + +namespace sourcemeta::core { + +auto sha256(const std::string_view input) -> std::string { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0))) { + throw std::runtime_error("Could not open the CNG SHA-256 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG SHA-256 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(input.data()))}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG SHA-256 digest"); + } + + std::string result; + result.reserve(64); + for (std::uint64_t index = 0; index < 32u; ++index) { + result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); + result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); + } + + return result; } } // namespace sourcemeta::core @@ -238,11 +344,15 @@ auto sha256(const std::string_view input) -> std::string { return result; } +} // namespace sourcemeta::core + +#endif + +namespace sourcemeta::core { + auto sha256(const std::string_view input, std::ostream &output) -> void { const auto result = sha256(input); output.write(result.data(), static_cast(result.size())); } } // namespace sourcemeta::core - -#endif diff --git a/src/core/crypto/crypto_uuid.cc b/src/core/crypto/crypto_uuid.cc index f572438d2..7f50a1a79 100644 --- a/src/core/crypto/crypto_uuid.cc +++ b/src/core/crypto/crypto_uuid.cc @@ -4,6 +4,27 @@ #include // std::size_t #include // std::string_view +#if defined(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) +#include // RAND_bytes + +#include // std::runtime_error +#elif defined(__APPLE__) +#include // errSecSuccess +#include // SecRandomCopyBytes, kSecRandomDefault + +#include // std::runtime_error +#elif defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::runtime_error +#else +#include // std::random_device, std::mt19937, std::uniform_int_distribution +#endif + namespace { constexpr auto is_hex_digit(const char character) -> bool { @@ -12,14 +33,36 @@ constexpr auto is_hex_digit(const char character) -> bool { (character >= 'A' && character <= 'F'); } -} // namespace - -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL -#include // RAND_bytes -#include // std::runtime_error +auto fill_random_bytes(std::array &bytes) -> void { +#if defined(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) + if (RAND_bytes(bytes.data(), static_cast(bytes.size())) != 1) { + throw std::runtime_error("Could not generate random bytes with OpenSSL"); + } +#elif defined(__APPLE__) + if (SecRandomCopyBytes(kSecRandomDefault, bytes.size(), bytes.data()) != + errSecSuccess) { + throw std::runtime_error( + "Could not generate random bytes with the Security framework"); + } +#elif defined(_WIN32) + if (!BCRYPT_SUCCESS(BCryptGenRandom(nullptr, bytes.data(), + static_cast(bytes.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG))) { + throw std::runtime_error("Could not generate random bytes with CNG"); + } #else -#include // std::random_device, std::mt19937, std::uniform_int_distribution + // Not a cryptographically secure generator. This fallback only exists to + // keep the module buildable on platforms without a system provider + thread_local std::random_device device; + thread_local std::mt19937 generator{device()}; + std::uniform_int_distribution distribution{0, 255}; + for (auto &byte : bytes) { + byte = static_cast(distribution(generator)); + } #endif +} + +} // namespace namespace sourcemeta::core { @@ -33,20 +76,8 @@ auto uuidv4() -> std::string { {false, false, false, false, true, false, true, false, true, false, true, false, false, false, false, false}}; -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL std::array random_bytes{}; - if (RAND_bytes(random_bytes.data(), static_cast(random_bytes.size())) != - 1) { - throw std::runtime_error("Could not generate random bytes with OpenSSL"); - } -#else - thread_local std::random_device device; - thread_local std::mt19937 generator{device()}; - std::uniform_int_distribution distribution(0, - 15); - std::uniform_int_distribution - variant_distribution(0, 3); -#endif + fill_random_bytes(random_bytes); std::string result; result.reserve(36); @@ -55,34 +86,20 @@ auto uuidv4() -> std::string { result += '-'; } -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL const auto high_nibble = (random_bytes[index] >> 4u) & 0x0fu; const auto low_nibble = random_bytes[index] & 0x0fu; -#endif // RFC 9562 Section 5.4: version bits (48-51) must be 0b0100 if (index == 6) { result += '4'; // RFC 9562 Section 5.4: variant bits (64-65) must be 0b10 } else if (index == 8) { -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL result += variant_digits[high_nibble & 0x03u]; -#else - result += variant_digits[variant_distribution(generator)]; -#endif } else { -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL result += digits[high_nibble]; -#else - result += digits[distribution(generator)]; -#endif } -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL result += digits[low_nibble]; -#else - result += digits[distribution(generator)]; -#endif } return result;