All checks were successful
Deploy / build-deploy (push) Successful in 3m47s
113 lines
5.2 KiB
C++
113 lines
5.2 KiB
C++
/*
|
|
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;
|
|
}
|