Crafter.Review/implementations/Crafter.Review-Crypto.cpp
Jorijn van der Graaf 36852edf63
feat: the crafter-review skeleton, and its semantics for text, JSON and crypto
What the rest of crafter-review stands on.

- project.cpp: a static crafter.review-lib and the crafter-review
  executable; Crafter.Network without QUIC, pinned, or with --local from
  the checkout next to this one; Crafter.LintRules' lite rules, pinned;
  the version from CRAFTER_REVIEW_VERSION, git describe or 0.0.0-dev.
- :Core and :Driver: the version, PromptVersion 1, --version.
- :Json: nlohmann's ordered_json, which keeps key order and unknown keys,
  so the files every driver on a machine shares survive a rewrite; dumps
  in the indented file layout, the spaced one-line form and the compact
  sorted form the structured-output schema needs byte for byte.
- :Crypto: SHA-256/SHA-1/MD5, HMAC, PBKDF2 through libcrypto; base64 and
  base32 written out, because what they refuse, and with which message,
  is pinned.
- :Unicode, Unicode 16.0.0's tables: whitespace, line breaks, decimal
  digits, printable characters, case mappings and case-insensitive folds.
- :Text: string functions on code points, number parsing, the shortest
  float form and half-even rounding, printed values, shell patterns, URL
  splitting and unquoting, POSIX paths, universal newlines and the
  replacing UTF-8 decoder.

Tests: Crypto (published vectors), Text (293 reference vectors).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:29:18 +01:00

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;
}
}