/* catcrafts.net Copyright (C) 2026 Catcrafts The source code of this website is made available for viewing purposes only. No permission is granted to copy, modify, distribute, or create derivative works. */ // Order tokens and the human-facing references derived from them. The token // is the only credential an order page has, so its shape check is a security // boundary: anything that is not exactly 32 lowercase hex characters must // never reach a lookup. import std; import Catcrafts.Shared; import Catcrafts.Server; using namespace Catcrafts; namespace { int failures = 0; void Check(bool ok, std::string_view what, std::string_view got = {}) { if (ok) return; ++failures; std::println(std::cerr, "FAIL: {}{}{}", what, got.empty() ? "" : " got: ", got); } } // namespace int main() { Check(IsOrderToken("0123456789abcdef0123456789abcdef"), "token: valid shape"); Check(!IsOrderToken("0123456789ABCDEF0123456789ABCDEF"), "token: uppercase rejected"); Check(!IsOrderToken("0123456789abcdef0123456789abcde"), "token: short rejected"); Check(!IsOrderToken("0123456789abcdef0123456789abcdeg"), "token: non-hex rejected"); const std::string tok = Server::NewOrderToken(); Check(IsOrderToken(tok), "token: generator emits valid tokens", tok); Check(Server::NewOrderToken() != tok, "token: not constant"); Check(Server::ReferenceFromToken("abcdef0123456789abcdef0123456789") == "CC-ABCDEF", "reference: derived and uppercased"); // ── the ISO 11649 creditor reference ────────────────────────────── // // The payer's own bank verifies these digits before the transfer leaves, // so a generator that computes them wrong is invisible here and rejected // at every bank in the country. Hence: verify our own output, verify the // verifier rejects tampering, and pin one value literally so a refactor // cannot quietly change the arithmetic. const std::string rf = Server::CreditorReferenceFromToken( "abcdef0123456789abcdef0123456789"); Check(Server::IsValidCreditorReference(rf), "creditor ref: generator output verifies", rf); Check(rf.starts_with("RF") && rf.size() == 12, "creditor ref: RF + 2 check digits + CCABCDEF", rf); Check(rf.substr(4) == "CCABCDEF", "creditor ref: body is the human reference without the hyphen", rf); Check(Server::CreditorReferenceFromToken("abcdef0123456789abcdef0123456789") == rf, "creditor ref: derived, so it is stable for one token"); // The guarantee mod-97-10 actually gives, asserted as the theorem it is // rather than as an empirical count: every single-character substitution // that keeps the character's CLASS is always caught. A letter contributes // two decimal digits (A=10 … Z=35) and a digit contributes one, so a // same-class change shifts the remainder by d*10^k or d*100^k with // |d| < 97; since 97 is prime that product is never ≡ 0, so the checksum // always moves. This is the case that matters — a donor retyping one // character of a reference is stopped by their own bank. // // A change that crosses classes (letter to digit) alters the length of the // decimal expansion and is therefore an ordinary 1-in-97 checksum bet, not // a guarantee. Exactly one such mutation of this reference does slip // through, which is the standard behaving as designed and not a defect; // asserting otherwise would be pinning an accident. int caught = 0, mutations = 0; for (std::size_t i = 2; i < rf.size(); ++i) { const bool isDigit = rf[i] >= '0' && rf[i] <= '9'; const std::string_view sameClass = isDigit ? "0123456789" : "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for (const char c : sameClass) { if (rf[i] == c) continue; std::string bad = rf; bad[i] = c; ++mutations; if (!Server::IsValidCreditorReference(bad)) ++caught; } } Check(mutations > 0 && caught == mutations, "creditor ref: every same-class one-character change fails verification", std::format("{} of {} caught", caught, mutations)); Check(!Server::IsValidCreditorReference("RF00CCABCDEF"), "creditor ref: wrong check digits rejected"); Check(!Server::IsValidCreditorReference("CCABCDEF"), "creditor ref: missing RF prefix rejected"); Check(!Server::IsValidCreditorReference("RF"), "creditor ref: too short rejected"); Check(!Server::IsValidCreditorReference(rf + "TOOLONGTOOLONGTOOLONGTOOLONG"), "creditor ref: over 25 characters rejected"); Check(!Server::IsValidCreditorReference("RF18CC-ABCDEF"), "creditor ref: non-alphanumeric body rejected"); // The canonical example from the standard's own documentation, so this is // pinned against an outside source and not only against ourselves. Check(Server::IsValidCreditorReference("RF18539007547034"), "creditor ref: the published ISO 11649 example verifies"); if (failures != 0) { std::println(std::cerr, "{} check(s) failed", failures); return 1; } return 0; }