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>
93 lines
5.8 KiB
C++
93 lines
5.8 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// The primitives the pipeline's keys, its Postgres login and its one-time
|
|
// codes rest on, against their published vectors: SHA-256, SHA-1 and MD5
|
|
// (FIPS 180, RFC 1321), HMAC (RFC 4231, RFC 2202), PBKDF2-HMAC-SHA256 (the
|
|
// RFC 7914 vectors), base32 (RFC 4648). A reuse key that hashed differently
|
|
// would silently stop every stored result from being reused; a wrong HMAC
|
|
// would lock the lens out of the database.
|
|
//
|
|
// base64 and base32 also have to fail with their fixed messages, because
|
|
// those reach the lens: a SCRAM exchange that cannot decode the server's
|
|
// salt, a TOTP secret that is not base32.
|
|
|
|
import std;
|
|
import Crafter.Review;
|
|
using namespace Crafter::Review;
|
|
|
|
namespace {
|
|
std::int32_t Failures = 0;
|
|
|
|
void Check(bool cond, std::string_view msg) {
|
|
if (!cond) {
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
|
++Failures;
|
|
}
|
|
}
|
|
|
|
std::string Unhex(std::string_view hex) {
|
|
std::string out;
|
|
for (std::size_t i = 0; i + 1 < hex.size(); i += 2) {
|
|
out.push_back(static_cast<char>(std::stoi(std::string(hex.substr(i, 2)), nullptr, 16)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// What the call threw as std::invalid_argument, or "" when it did not.
|
|
template <typename F>
|
|
std::string Refusal(F call) {
|
|
try {
|
|
call();
|
|
} catch (const std::invalid_argument& e) {
|
|
return e.what();
|
|
}
|
|
return "";
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
Check(Sha256Hex("") == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "sha256 of the empty string");
|
|
Check(Sha256Hex("abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "sha256 of abc");
|
|
Check(HexEncode(Sha1("abc")) == "a9993e364706816aba3e25717850c26c9cd0d89d", "sha1 of abc");
|
|
Check(Md5Hex("abc") == "900150983cd24fb0d6963f7d28e17f72", "md5 of abc");
|
|
|
|
const std::string key20(20, '\x0b');
|
|
Check(HexEncode(HmacSha256(key20, "Hi There")) == "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", "HMAC-SHA256, RFC 4231 case 1");
|
|
Check(HexEncode(HmacSha256("Jefe", "what do ya want for nothing?")) == "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", "HMAC-SHA256, RFC 4231 case 2");
|
|
Check(HexEncode(HmacSha1(key20, "Hi There")) == "b617318655057264e28bc0b6fb378c8ef146be00", "HMAC-SHA1, RFC 2202 case 1");
|
|
|
|
Check(HexEncode(Pbkdf2Sha256("password", "salt", 1)) == "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b", "PBKDF2-HMAC-SHA256, 1 iteration");
|
|
Check(HexEncode(Pbkdf2Sha256("password", "salt", 4096)) == "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a", "PBKDF2-HMAC-SHA256, 4096 iterations");
|
|
|
|
Check(Base64Encode("") == "" && Base64Encode("f") == "Zg==" && Base64Encode("fo") == "Zm8=" && Base64Encode("foo") == "Zm9v" && Base64Encode("foobar") == "Zm9vYmFy", "base64 encoding, RFC 4648 vectors");
|
|
Check(Base64Decode("Zm9vYmFy") == "foobar" && Base64Decode("Zg==") == "f" && Base64Decode("Zm8=") == "fo", "base64 decoding");
|
|
Check(Base64Decode("Zm9v\nYmFy!") == "foobar", "characters outside the alphabet are skipped, as b64decode skips them");
|
|
Check(Base64Decode("YQ==YmM=") == "a", "a complete padding sequence ends the data");
|
|
Check(Refusal([] { Base64Decode("Zm9vY"); }) == "Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4", "one data character too many: binascii's message");
|
|
Check(Refusal([] { Base64Decode("Zm9vYm"); }) == "Incorrect padding", "two left over without padding: Incorrect padding");
|
|
const std::string salt = Base64Decode("W22ZaJ0SNY7soEsUEjb6gQ==");
|
|
Check(salt.size() == 16 && Base64Encode(salt) == "W22ZaJ0SNY7soEsUEjb6gQ==", "the RFC 7677 salt round-trips");
|
|
|
|
Check(Base32Decode("") == "", "base32 of nothing");
|
|
Check(Base32Decode("MY======") == "f" && Base32Decode("MZXQ====") == "fo" && Base32Decode("MZXW6===") == "foo", "base32, RFC 4648 vectors (1-3 bytes)");
|
|
Check(Base32Decode("MZXW6YQ=") == "foob" && Base32Decode("MZXW6YTB") == "fooba" && Base32Decode("MZXW6YTBOI======") == "foobar", "base32, RFC 4648 vectors (4-6 bytes)");
|
|
Check(Base32Decode("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ") == "12345678901234567890", "base32 of the RFC 6238 secret");
|
|
Check(Refusal([] { Base32Decode("MZXW6Y=="); }) == "Incorrect padding", "two padding characters are not a legal amount");
|
|
Check(Refusal([] { Base32Decode("MZXW6"); }) == "Incorrect padding", "a length that is not a multiple of 8");
|
|
Check(Refusal([] { Base32Decode("mzxw6ytb"); }) == "Non-base32 digit found", "lower case is not the alphabet (b32decode without casefold)");
|
|
Check(Refusal([] { Base32Decode("1!"); }) == "Non-base32 digit found", "a bad digit is reported before a bad length, as b32decode reports it");
|
|
|
|
Check(Unhex("00ff") == std::string("\x00\xff", 2) && HexEncode(std::string("\x00\xff", 2)) == "00ff", "hex both ways, lowercase");
|
|
Check(RandomBytes(18).size() == 18 && RandomBytes(0).empty(), "random bytes of the length asked for");
|
|
Check(RandomBytes(16) != RandomBytes(16), "two draws differ");
|
|
const std::string token = TokenHex(16);
|
|
Check(token.size() == 32 && std::ranges::all_of(token, [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); }), "token_hex(16) is 32 lowercase hex digits");
|
|
const std::string uuid = Uuid4();
|
|
Check(uuid.size() == 36 && uuid[8] == '-' && uuid[13] == '-' && uuid[18] == '-' && uuid[23] == '-', "a uuid4 is 8-4-4-4-12");
|
|
Check(uuid[14] == '4' && std::string_view("89ab").contains(uuid[19]), "version 4, RFC 4122 variant");
|
|
|
|
Check(ConstantTimeEqual("abc", "abc") && !ConstantTimeEqual("abc", "abd") && !ConstantTimeEqual("abc", "ab"), "constant-time comparison");
|
|
|
|
return Failures == 0 ? 0 : 1;
|
|
}
|