239 lines
10 KiB
C++
239 lines
10 KiB
C++
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
module;
|
||
|
|
#include <openssl/crypto.h>
|
||
|
|
#include <openssl/evp.h>
|
||
|
|
#include <openssl/hmac.h>
|
||
|
|
#include <sys/random.h>
|
||
|
|
#include <cerrno>
|
||
|
|
|
||
|
|
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<unsigned char, EVP_MAX_MD_SIZE> 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<const char*>(out.data()), length);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Hmac(const EVP_MD* method, std::string_view key, std::string_view data) {
|
||
|
|
std::array<unsigned char, EVP_MAX_MD_SIZE> out {};
|
||
|
|
std::uint32_t length = 0;
|
||
|
|
const unsigned char* result = HMAC(method, key.data(), static_cast<int>(key.size()),
|
||
|
|
reinterpret_cast<const unsigned char*>(data.data()), data.size(),
|
||
|
|
out.data(), &length);
|
||
|
|
if (result == nullptr) throw std::runtime_error("libcrypto: HMAC failed");
|
||
|
|
return std::string(reinterpret_cast<const char*>(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<std::int32_t>(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<unsigned char, 32> out {};
|
||
|
|
if (PKCS5_PBKDF2_HMAC(password.data(), static_cast<std::int32_t>(password.size()),
|
||
|
|
reinterpret_cast<const std::uint8_t*>(salt.data()), static_cast<std::int32_t>(salt.size()),
|
||
|
|
static_cast<std::int32_t>(iterations), EVP_sha256(), static_cast<std::int32_t>(out.size()), out.data()) != 1) {
|
||
|
|
throw std::runtime_error("libcrypto: PBKDF2 failed");
|
||
|
|
}
|
||
|
|
return std::string(reinterpret_cast<const char*>(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<std::uint8_t>(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<std::uint8_t>(bytes[i]) << 16) | (static_cast<std::uint8_t>(bytes[i + 1]) << 8) | static_cast<std::uint8_t>(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<std::uint8_t>(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<std::uint8_t>(bytes[i]) << 16) | (static_cast<std::uint8_t>(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<std::uint8_t>(value);
|
||
|
|
pads = 0;
|
||
|
|
switch (quadPosition) {
|
||
|
|
case 0:
|
||
|
|
quadPosition = 1;
|
||
|
|
leftChar = bits;
|
||
|
|
break;
|
||
|
|
case 1:
|
||
|
|
quadPosition = 2;
|
||
|
|
out.push_back(static_cast<char>(static_cast<std::uint8_t>(leftChar << 2) | (bits >> 4)));
|
||
|
|
leftChar = bits & 0x0F;
|
||
|
|
break;
|
||
|
|
case 2:
|
||
|
|
quadPosition = 3;
|
||
|
|
out.push_back(static_cast<char>(static_cast<std::uint8_t>(leftChar << 4) | (bits >> 2)));
|
||
|
|
leftChar = bits & 0x03;
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
quadPosition = 0;
|
||
|
|
out.push_back(static_cast<char>(static_cast<std::uint8_t>(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<char>((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<char>((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<std::size_t>(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<char>((static_cast<std::uint8_t>(bytes[6]) & 0x0F) | 0x40);
|
||
|
|
bytes[8] = static_cast<char>((static_cast<std::uint8_t>(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;
|
||
|
|
}
|
||
|
|
}
|