// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include #include #include #include #include module Crafter.Review:Crypto_impl; import :Crypto; import std; namespace Crafter::Review { namespace { std::string Digest(const EVP_MD* method, std::string_view data) { std::array out {}; std::uint32_t length = 0; if (EVP_Digest(data.data(), data.size(), out.data(), &length, method, nullptr) != 1) { throw std::runtime_error("libcrypto: digest failed"); } return std::string(reinterpret_cast(out.data()), length); } std::string Hmac(const EVP_MD* method, std::string_view key, std::string_view data) { std::array out {}; std::uint32_t length = 0; const unsigned char* result = HMAC(method, key.data(), static_cast(key.size()), reinterpret_cast(data.data()), data.size(), out.data(), &length); if (result == nullptr) throw std::runtime_error("libcrypto: HMAC failed"); return std::string(reinterpret_cast(out.data()), length); } constexpr std::string_view Base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; constexpr std::string_view Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; std::int32_t Base64Value(char c) { const std::size_t at = Base64Alphabet.find(c); return at == std::string_view::npos ? -1 : static_cast(at); } } std::string Sha256(std::string_view data) { return Digest(EVP_sha256(), data); } std::string Sha1(std::string_view data) { return Digest(EVP_sha1(), data); } std::string Md5(std::string_view data) { return Digest(EVP_md5(), data); } std::string Sha256Hex(std::string_view data) { return HexEncode(Sha256(data)); } std::string Md5Hex(std::string_view data) { return HexEncode(Md5(data)); } std::string HmacSha256(std::string_view key, std::string_view data) { return Hmac(EVP_sha256(), key, data); } std::string HmacSha1(std::string_view key, std::string_view data) { return Hmac(EVP_sha1(), key, data); } std::string Pbkdf2Sha256(std::string_view password, std::string_view salt, std::uint32_t iterations) { std::array out {}; if (PKCS5_PBKDF2_HMAC(password.data(), static_cast(password.size()), reinterpret_cast(salt.data()), static_cast(salt.size()), static_cast(iterations), EVP_sha256(), static_cast(out.size()), out.data()) != 1) { throw std::runtime_error("libcrypto: PBKDF2 failed"); } return std::string(reinterpret_cast(out.data()), out.size()); } std::string HexEncode(std::string_view bytes) { constexpr std::string_view Digits = "0123456789abcdef"; std::string out; out.reserve(bytes.size() * 2); for (const char c : bytes) { const auto byte = static_cast(c); out.push_back(Digits[byte >> 4]); out.push_back(Digits[byte & 0x0F]); } return out; } std::string Base64Encode(std::string_view bytes) { std::string out; out.reserve((bytes.size() + 2) / 3 * 4); std::size_t i = 0; for (; i + 3 <= bytes.size(); i += 3) { const std::uint32_t triple = (static_cast(bytes[i]) << 16) | (static_cast(bytes[i + 1]) << 8) | static_cast(bytes[i + 2]); out.push_back(Base64Alphabet[(triple >> 18) & 0x3F]); out.push_back(Base64Alphabet[(triple >> 12) & 0x3F]); out.push_back(Base64Alphabet[(triple >> 6) & 0x3F]); out.push_back(Base64Alphabet[triple & 0x3F]); } const std::size_t left = bytes.size() - i; if (left == 1) { const std::uint32_t one = static_cast(bytes[i]) << 16; out.push_back(Base64Alphabet[(one >> 18) & 0x3F]); out.push_back(Base64Alphabet[(one >> 12) & 0x3F]); out += "=="; } else if (left == 2) { const std::uint32_t two = (static_cast(bytes[i]) << 16) | (static_cast(bytes[i + 1]) << 8); out.push_back(Base64Alphabet[(two >> 18) & 0x3F]); out.push_back(Base64Alphabet[(two >> 12) & 0x3F]); out.push_back(Base64Alphabet[(two >> 6) & 0x3F]); out.push_back('='); } return out; } std::string Base64Decode(std::string_view text) { // binascii.a2b_base64 in its default, non-strict mode, which is what // b64decode runs: a character outside the alphabet is skipped, a '=' // only counts once two characters of a quad are in, and a complete // padding sequence ends the data — whatever follows it is ignored. // What a quad has left at the end decides the error. std::string out; std::int32_t quadPosition = 0; std::uint8_t leftChar = 0; std::int32_t pads = 0; for (const char c : text) { if (c == '=') { if (quadPosition >= 2 && quadPosition + ++pads >= 4) return out; continue; } const std::int32_t value = Base64Value(c); if (value < 0) continue; const auto bits = static_cast(value); pads = 0; switch (quadPosition) { case 0: quadPosition = 1; leftChar = bits; break; case 1: quadPosition = 2; out.push_back(static_cast(static_cast(leftChar << 2) | (bits >> 4))); leftChar = bits & 0x0F; break; case 2: quadPosition = 3; out.push_back(static_cast(static_cast(leftChar << 4) | (bits >> 2))); leftChar = bits & 0x03; break; default: quadPosition = 0; out.push_back(static_cast(static_cast(leftChar << 6) | bits)); leftChar = 0; break; } } if (quadPosition == 1) { throw std::invalid_argument(std::format("Invalid base64-encoded string: number of data characters ({}) cannot be 1 more than a multiple of 4", out.size() / 3 * 4 + 1)); } if (quadPosition != 0) throw std::invalid_argument("Incorrect padding"); return out; } std::string Base32Decode(std::string_view text) { // base64.b32decode, in its order: the digits are checked while the // quanta are decoded, the length and the padding after. std::string_view data = text; while (!data.empty() && data.back() == '=') data.remove_suffix(1); const std::size_t padChars = text.size() - data.size(); std::string out; std::uint64_t accumulator = 0; for (std::size_t start = 0; start < data.size(); start += 8) { accumulator = 0; for (const char c : data.substr(start, 8)) { const std::size_t value = Base32Alphabet.find(c); if (value == std::string_view::npos) throw std::invalid_argument("Non-base32 digit found"); accumulator = (accumulator << 5) | value; } for (std::int32_t shift = 32; shift >= 0; shift -= 8) { out.push_back(static_cast((accumulator >> shift) & 0xFF)); } } if (text.size() % 8 != 0 || (padChars != 0 && padChars != 1 && padChars != 3 && padChars != 4 && padChars != 6)) { throw std::invalid_argument("Incorrect padding"); } if (padChars != 0 && !out.empty()) { // b32decode decoded the partial quantum as if it were 8 digits // short of its padding: redo the last 5 bytes from the // accumulator shifted by the padding, and keep what the data // characters carried. const std::uint64_t shifted = accumulator << (5 * padChars); std::string last; for (std::int32_t shift = 32; shift >= 0; shift -= 8) { last.push_back(static_cast((shifted >> shift) & 0xFF)); } const std::size_t leftover = (43 - 5 * padChars) / 8; out.resize(out.size() - 5); out += last.substr(0, leftover); } return out; } std::string RandomBytes(std::size_t count) { std::string out(count, '\0'); std::size_t filled = 0; while (filled < count) { const ssize_t got = getrandom(out.data() + filled, count - filled, 0); if (got < 0) { if (errno == EINTR) continue; throw std::runtime_error(std::format("getrandom: {}", std::strerror(errno))); } filled += static_cast(got); } return out; } std::string TokenHex(std::size_t count) { return HexEncode(RandomBytes(count)); } std::string Uuid4() { std::string bytes = RandomBytes(16); bytes[6] = static_cast((static_cast(bytes[6]) & 0x0F) | 0x40); bytes[8] = static_cast((static_cast(bytes[8]) & 0x3F) | 0x80); const std::string hex = HexEncode(bytes); return std::format("{}-{}-{}-{}-{}", hex.substr(0, 8), hex.substr(8, 4), hex.substr(12, 4), hex.substr(16, 4), hex.substr(20)); } bool ConstantTimeEqual(std::string_view a, std::string_view b) { if (a.size() != b.size()) return false; return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0; } }