Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
main.cpp
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | "ckey/ckey.h" |
| 2 | "print_output.h" |
| 3 | <cstdint> |
| 4 | <cstring> |
| 5 | <ctime> |
| 6 | <cstdio> |
| 7 | <fstream> |
| 8 | <iostream> |
| 9 | <memory> |
| 10 | <cmath> |
| 11 | <cstdlib> |
| 12 | <string> |
| 13 | <vector> |
| 14 | |
| 15 | static int hex_digit(char c) { |
| 16 | if (c >= '0' && c <= '9') return c - '0'; |
| 17 | if (c >= 'a' && c <= 'f') return c - 'a' + 10; |
| 18 | if (c >= 'A' && c <= 'F') return c - 'A' + 10; |
| 19 | return -1; |
| 20 | } |
| 21 | |
| 22 | // Strict: an even number of hexadecimal digits and nothing else (no sign, |
| 23 | // whitespace, or 0x prefix). |
| 24 | static bool from_hex(const std::string &input, std::vector<uint8_t> &out) { |
| 25 | if (input.size() % 2 != 0) return false; |
| 26 | out.resize(input.size() / 2); |
| 27 | for (size_t i = 0; i < out.size(); ++i) { |
| 28 | const int hi = hex_digit(input[2 * i]), lo = hex_digit(input[2 * i + 1]); |
| 29 | if (hi < 0 || lo < 0) return false; |
| 30 | out[i] = static_cast<uint8_t>(hi << 4 | lo); |
| 31 | } |
| 32 | return true; |
| 33 | } |
| 34 | |
| 35 | static bool write_svg(const std::string &path, const std::string &svg) { |
| 36 | std::ofstream file(path, std::ios::binary); |
| 37 | if (!file) return false; |
| 38 | file.write(svg.data(), static_cast<std::streamsize>(svg.size() - 1)); |
| 39 | return static_cast<bool>(file); |
| 40 | } |
| 41 | |
| 42 | static std::string base64(const std::string &in) { |
| 43 | static const char *t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
| 44 | std::string out; size_t i = 0; |
| 45 | for (; i + 2 < in.size(); i += 3) { |
| 46 | const uint32_t v = (uint8_t(in[i]) << 16) | (uint8_t(in[i + 1]) << 8) | uint8_t(in[i + 2]); |
| 47 | out += t[v >> 18]; out += t[(v >> 12) & 63]; out += t[(v >> 6) & 63]; out += t[v & 63]; |
| 48 | } |
| 49 | if (i < in.size()) { |
| 50 | const uint32_t v = (uint8_t(in[i]) << 16) | (i + 1 < in.size() ? uint8_t(in[i + 1]) << 8 : 0); |
| 51 | out += t[v >> 18]; out += t[(v >> 12) & 63]; out += i + 1 < in.size() ? t[(v >> 6) & 63] : '='; out += '='; |
| 52 | } |
| 53 | return out; |
| 54 | } |
| 55 | |
| 56 | // ckey encode-scalable (--utf8 <text> | --hex <hex>) --out <file.svg> |
| 57 | // [--version 1-31] [--level L|M|Q|H] [--preset plain|concept] |
| 58 | // [--modules rounded|square|circle|diamond] [--rings joined|dots] [--rays round|flat] |
| 59 | // [--logo <png|jpg|svg file>] [--logo-radius <units>] |
| 60 | static int encode_scalable(int argc, char **argv) { |
| 61 | std::string mode, value, output, logo_href, png, raster_out, pdf_out; uint32_t version = 0; ckey_ecc_level level = CKEY_ECC_M; |
| 62 | bool transparent = false; ckey_raster_policy policy = CKEY_RASTER_COVERAGE; uint32_t size_px = 1024; |
| 63 | double size_in = 1.0, dpi = 600; |
| 64 | ckey_scalable_layout layout = CKEY_LAYOUT_STANDARD; |
| 65 | ckey_style style; ckey_style_default(&style); |
| 66 | for (int i = 2; i + 1 < argc; i += 2) { |
| 67 | const std::string flag = argv[i], arg = argv[i + 1]; |
| 68 | if (flag == "--utf8" || flag == "--hex") { mode = flag; value = arg; } |
| 69 | else if (flag == "--out") output = arg; |
| 70 | else if (flag == "--version") { |
| 71 | char *end = nullptr; const long v = std::strtol(arg.c_str(), &end, 10); |
| 72 | if (!end || *end || v < 1 || v > static_cast<long>(CKEY_SCALABLE_VERSION_COUNT)) { std::cerr << "--version must be 1 to " << CKEY_SCALABLE_VERSION_COUNT << '\n'; return 2; } |
| 73 | version = static_cast<uint32_t>(v); |
| 74 | } else if (flag == "--level") { |
| 75 | if (arg == "L") level = CKEY_ECC_L; else if (arg == "M") level = CKEY_ECC_M; else if (arg == "Q") level = CKEY_ECC_Q; else if (arg == "H") level = CKEY_ECC_H; |
| 76 | else { std::cerr << "--level must be L, M, Q, or H\n"; return 2; } |
| 77 | } else if (flag == "--png") { png = arg; |
| 78 | } else if (flag == "--raster") { raster_out = arg; |
| 79 | } else if (flag == "--pdf") { pdf_out = arg; |
| 80 | } else if (flag == "--transparent") { |
| 81 | if (arg == "yes") transparent = true; else if (arg == "no") transparent = false; else { std::cerr << "--transparent must be yes or no\n"; return 2; } |
| 82 | } else if (flag == "--policy") { |
| 83 | if (arg == "coverage") policy = CKEY_RASTER_COVERAGE; else if (arg == "binary") policy = CKEY_RASTER_BINARY; else if (arg == "hybrid") policy = CKEY_RASTER_HYBRID; |
| 84 | else { std::cerr << "--policy must be coverage, binary, or hybrid\n"; return 2; } |
| 85 | } else if (flag == "--size-px") { |
| 86 | char *end = nullptr; const long v = std::strtol(arg.c_str(), &end, 10); |
| 87 | if (!end || *end || v < 16 || v > 16384) { std::cerr << "--size-px must be 16 to 16384\n"; return 2; } |
| 88 | size_px = static_cast<uint32_t>(v); |
| 89 | } else if (flag == "--size-in" || flag == "--dpi") { |
| 90 | char *end = nullptr; const double v = std::strtod(arg.c_str(), &end); |
| 91 | if (!end || *end || !(v > 0)) { std::cerr << flag << " must be a positive number\n"; return 2; } |
| 92 | (flag == "--dpi" ? dpi : size_in) = v; |
| 93 | } else if (flag == "--layout") { |
| 94 | if (arg == "standard") layout = CKEY_LAYOUT_STANDARD; else if (arg == "print") layout = CKEY_LAYOUT_PRINT; |
| 95 | else { std::cerr << "--layout must be standard or print\n"; return 2; } |
| 96 | } else if (flag == "--preset") { |
| 97 | if (arg == "concept") ckey_style_preset_fill(&style, CKEY_PRESET_CONCEPT); |
| 98 | else if (arg == "plain") ckey_style_preset_fill(&style, CKEY_PRESET_PLAIN); |
| 99 | else { std::cerr << "--preset must be plain or concept\n"; return 2; } |
| 100 | } else if (flag == "--modules") { |
| 101 | if (arg == "rounded") style.modules = CKEY_MODULE_ROUNDED; else if (arg == "square") style.modules = CKEY_MODULE_SQUARE; |
| 102 | else if (arg == "circle") style.modules = CKEY_MODULE_CIRCLE; else if (arg == "diamond") style.modules = CKEY_MODULE_DIAMOND; |
| 103 | else { std::cerr << "--modules must be rounded, square, circle, or diamond\n"; return 2; } |
| 104 | } else if (flag == "--rings") { |
| 105 | if (arg == "joined") style.rings = CKEY_RING_JOINED; else if (arg == "dots") style.rings = CKEY_RING_DOTS; |
| 106 | else { std::cerr << "--rings must be joined or dots\n"; return 2; } |
| 107 | } else if (flag == "--rays") { |
| 108 | if (arg == "round") style.rays = CKEY_RAY_ROUND; else if (arg == "flat") style.rays = CKEY_RAY_FLAT; |
| 109 | else { std::cerr << "--rays must be round or flat\n"; return 2; } |
| 110 | } else if (flag == "--logo") { |
| 111 | std::ifstream file(arg, std::ios::binary); |
| 112 | if (!file) { std::cerr << "cannot read logo " << arg << '\n'; return 2; } |
| 113 | const std::string bytes((std::istreambuf_iterator<char>(file)), {}); |
| 114 | const std::string ext = arg.size() > 4 ? arg.substr(arg.size() - 4) : ""; |
| 115 | const std::string type = ext == ".svg" ? "image/svg+xml" : ext == ".png" ? "image/png" : "image/jpeg"; |
| 116 | logo_href = "data:" + type + ";base64," + base64(bytes); style.logo_plate = 1; |
| 117 | } else if (flag == "--logo-radius") { |
| 118 | char *end = nullptr; style.logo_radius = std::strtod(arg.c_str(), &end); |
| 119 | if (!end || *end || !(style.logo_radius >= 0)) { std::cerr << "--logo-radius must be a non-negative number\n"; return 2; } |
| 120 | } else { std::cerr << "unknown option " << flag << '\n'; return 2; } |
| 121 | } |
| 122 | if (!logo_href.empty()) style.logo_href = logo_href.c_str(); |
| 123 | if ((argc % 2) != 0 || mode.empty() || (output.empty() && png.empty() && raster_out.empty() && pdf_out.empty())) { |
| 124 | std::cerr << "usage: ckey encode-scalable (--utf8 <text> | --hex <hex>) --out <file.svg> [--version 1-31] [--level L|M|Q|H] [--layout standard|print]\n" |
| 125 | " [--preset plain|concept] [--modules rounded|square|circle|diamond] [--rings joined|dots] [--rays round|flat]\n" |
| 126 | " [--logo <file>] [--logo-radius <units>]\n" |
| 127 | " [--raster <file.png> [--size-px N] [--policy coverage|binary|hybrid] [--transparent yes|no]] [--pdf <file.pdf>]\n" |
| 128 | " [--png <print.png> [--size-in IN] [--dpi DPI]]\n"; return 2; |
| 129 | } |
| 130 | std::vector<uint8_t> payload; |
| 131 | if (mode == "--hex") { if (!from_hex(value, payload)) { std::cerr << "hex input must be an even number of hexadecimal digits\n"; return 2; } } |
| 132 | else payload.assign(value.begin(), value.end()); |
| 133 | auto symbol = std::make_unique<ckey_scalable_symbol>(); ckey_diagnostics diag{}; |
| 134 | if (ckey_encode_scalable(payload.data(), payload.size(), version, level, layout, symbol.get(), &diag) != CKEY_OK) { std::cerr << diag.message << '\n'; return 1; } |
| 135 | ckey_diagnostics check{}; |
| 136 | if (ckey_check_scalable_style(symbol.get(), &style, &check) != CKEY_OK) { std::cerr << "design refused: " << check.message << '\n'; return 1; } |
| 137 | size_t needed = 0; ckey_render_scalable_styled_svg(symbol.get(), &style, 16, nullptr, 0, &needed); |
| 138 | std::string svg(needed, '\0'); |
| 139 | if (ckey_render_scalable_styled_svg(symbol.get(), &style, 16, svg.data(), svg.size(), &needed) != CKEY_OK || (!output.empty() && !write_svg(output, svg))) { std::cerr << "SVG rendering failed\n"; return 1; } |
| 140 | if (!raster_out.empty()) { |
| 141 | // Screen raster straight from geometry (no SVG). A logo file is not |
| 142 | // decoded here, so the raster shows only its plate. |
| 143 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 144 | o.size_px = size_px; o.policy = policy; o.transparent = transparent ? 1u : 0u; |
| 145 | size_t need = 0; |
| 146 | if (ckey_render_scalable_raster(symbol.get(), &style, &o, nullptr, 0, &need) != CKEY_INSUFFICIENT_BUFFER) { std::cerr << "raster rendering failed\n"; return 1; } |
| 147 | std::vector<uint8_t> px(need); |
| 148 | size_t png_need = 0; |
| 149 | if (ckey_render_scalable_raster(symbol.get(), &style, &o, px.data(), px.size(), &need) != CKEY_OK || |
| 150 | ckey_encode_png(px.data(), size_px, size_px, 0, CKEY_PNG_RGBA8, 0, nullptr, 0, &png_need) != CKEY_INSUFFICIENT_BUFFER) { std::cerr << "raster rendering failed\n"; return 1; } |
| 151 | std::vector<uint8_t> file(png_need); |
| 152 | if (ckey_encode_png(px.data(), size_px, size_px, 0, CKEY_PNG_RGBA8, 0, file.data(), file.size(), &png_need) != CKEY_OK) { std::cerr << "PNG encoding failed\n"; return 1; } |
| 153 | std::ofstream f(raster_out, std::ios::binary); f.write(reinterpret_cast<const char *>(file.data()), static_cast<std::streamsize>(file.size())); |
| 154 | if (!f) { std::cerr << "could not write " << raster_out << '\n'; return 1; } |
| 155 | std::cout << "raster PNG: " << raster_out << " (" << size_px << " px" << (transparent ? ", transparent" : "") << ")\n"; |
| 156 | } |
| 157 | if (!pdf_out.empty()) { |
| 158 | size_t need = 0; |
| 159 | if (ckey_render_scalable_pdf(symbol.get(), &style, size_in * 72.0, transparent ? 1u : 0u, nullptr, 0, &need) != CKEY_INSUFFICIENT_BUFFER) { std::cerr << "PDF rendering failed\n"; return 1; } |
| 160 | std::vector<uint8_t> file(need); |
| 161 | if (ckey_render_scalable_pdf(symbol.get(), &style, size_in * 72.0, transparent ? 1u : 0u, file.data(), file.size(), &need) != CKEY_OK) { std::cerr << "PDF rendering failed\n"; return 1; } |
| 162 | std::ofstream f(pdf_out, std::ios::binary); f.write(reinterpret_cast<const char *>(file.data()), static_cast<std::streamsize>(file.size())); |
| 163 | if (!f) { std::cerr << "could not write " << pdf_out << '\n'; return 1; } |
| 164 | std::cout << "vector PDF: " << pdf_out << " (" << size_in << " in)\n"; |
| 165 | } |
| 166 | if (!png.empty()) { |
| 167 | // Print export: black ink on paper on the printer's dot grid. Colours, |
| 168 | // materials, and logo images are screen styling and are not printed. |
| 169 | printout::Page page; page.width = page.height = static_cast<int>(std::lround(size_in * dpi)); page.ink.assign(static_cast<size_t>(page.width) * page.height, 0); |
| 170 | if (!printout::draw_symbol(page, *symbol, &style, 0, 0, page.width, dpi) || !printout::write_png(png, page, dpi)) { std::cerr << "could not write " << png << '\n'; return 1; } |
| 171 | std::cout << "print PNG: " << png << " (" << size_in << " in at " << dpi << " dpi, 1-bit, black on paper)\n"; |
| 172 | } |
| 173 | std::cout << diag.message << "; profile=0" << (layout == CKEY_LAYOUT_PRINT ? ".1" : "") << "; format=0." |
| 174 | << (layout == CKEY_LAYOUT_PRINT ? CKEY_SCALABLE_PRINT_FORMAT_VERSION : CKEY_SCALABLE_FORMAT_VERSION) << ".0; version=" << symbol->version |
| 175 | << "; payload-bytes=" << payload.size() << "; capacity=" << ckey_scalable_capacity(symbol->version, level, layout) << "; svg=" << output << '\n'; |
| 176 | return 0; |
| 177 | } |
| 178 | |
| 179 | // ckey print-sheet --out sheet.png [--dpi 600] [--set small|large] |
| 180 | // A US Letter calibration ladder of P0 and P0.1 symbols at several payloads |
| 181 | // and printed sizes, as a 1-bit device-grid PNG. Each symbol's payload names |
| 182 | // its own cell ("CKEY-CAL P0.1 120B 0.50in ..."), so a scan identifies it. |
| 183 | static int print_sheet(int argc, char **argv) { |
| 184 | std::string out, set = "small"; double dpi = 600; |
| 185 | for (int i = 2; i + 1 < argc; i += 2) { |
| 186 | const std::string flag = argv[i], arg = argv[i + 1]; |
| 187 | if (flag == "--out") out = arg; else if (flag == "--set") set = arg; |
| 188 | else if (flag == "--dpi") { dpi = std::strtod(arg.c_str(), nullptr); if (!(dpi >= 150 && dpi <= 2400)) { std::cerr << "--dpi must be 150..2400\n"; return 2; } } |
| 189 | else { std::cerr << "unknown option " << flag << '\n'; return 2; } |
| 190 | } |
| 191 | if (out.empty() || (set != "small" && set != "large") || (argc % 2) != 0) { std::cerr << "usage: ckey print-sheet --out sheet.png [--dpi 600] [--set small|large]\n"; return 2; } |
| 192 | const std::vector<size_t> payloads = set == "small" ? std::vector<size_t>{8, 30, 120, 400} : std::vector<size_t>{1200, 2400}; |
| 193 | const std::vector<double> sizes = set == "small" ? std::vector<double>{0.25, 0.375, 0.5, 0.75, 1.0} : std::vector<double>{1.0, 1.25, 1.5, 2.0}; |
| 194 | printout::Page page; page.width = static_cast<int>(std::lround(8.5 * dpi)); page.height = static_cast<int>(std::lround(11 * dpi)); |
| 195 | page.ink.assign(static_cast<size_t>(page.width) * page.height, 0); |
| 196 | const int text = std::max(2, static_cast<int>(std::lround(dpi / 150))); // label scale |
| 197 | const auto in = [&](double v) { return static_cast<int>(std::lround(v * dpi)); }; |
| 198 | printout::draw_text(page, "CKey print calibration - print at 100% actual size", in(0.5), in(0.35), text + 1); |
| 199 | printout::draw_text(page, "dpi " + std::to_string(static_cast<int>(dpi)) + " level M black on paper", in(0.5), in(0.35) + 9 * (text + 1), text); |
| 200 | int y = in(0.9); const double max_size = sizes.back(); |
| 201 | for (size_t bytes : payloads) for (auto layout : {CKEY_LAYOUT_STANDARD, CKEY_LAYOUT_PRINT}) { |
| 202 | printout::draw_text(page, std::string(layout ? "P0.1 " : "P0 ") + std::to_string(bytes) + "B", in(0.5), y + in(max_size / 2), text); |
| 203 | int x = in(1.5); |
| 204 | for (double size : sizes) { |
| 205 | char label[64]; std::snprintf(label, sizeof(label), "CKEY-CAL %s %zuB %.2fin ", layout ? "P0.1" : "P0", bytes, size); |
| 206 | std::string payload = label; while (payload.size() < bytes) payload += "0123456789"; |
| 207 | payload.resize(bytes); |
| 208 | auto symbol = std::make_unique<ckey_scalable_symbol>(); ckey_diagnostics diag{}; |
| 209 | if (ckey_encode_scalable(reinterpret_cast<const uint8_t *>(payload.data()), payload.size(), 0, CKEY_ECC_M, layout, symbol.get(), &diag) != CKEY_OK) { |
| 210 | std::cerr << "cannot encode " << label << ": " << diag.message << '\n'; return 1; |
| 211 | } |
| 212 | if (!printout::draw_symbol(page, *symbol, nullptr, x, y, in(size), dpi)) { std::cerr << "cannot render " << label << '\n'; return 1; } |
| 213 | char caption[32]; std::snprintf(caption, sizeof(caption), "v%u %.2fin", symbol->version, size); |
| 214 | printout::draw_text(page, caption, x, y + in(size) + text * 2, text); |
| 215 | x += in(size) + in(0.3); |
| 216 | } |
| 217 | y += in(max_size) + in(0.3); |
| 218 | } |
| 219 | if (!printout::write_png(out, page, dpi)) { std::cerr << "could not write " << out << '\n'; return 1; } |
| 220 | std::cout << "calibration sheet: " << out << " (US Letter, " << dpi << " dpi, 1-bit). Print at 100% / actual size.\n"; |
| 221 | return 0; |
| 222 | } |
| 223 | |
| 224 | // Safe Actions (spec/CKey-Safe-Actions.md) for issuers: make a key, sign, verify. |
| 225 | static std::string to_hex(const uint8_t *p, size_t n) { |
| 226 | static const char d[] = "0123456789abcdef"; |
| 227 | std::string s; |
| 228 | for (size_t i = 0; i < n; ++i) { s.push_back(d[p[i] >> 4]); s.push_back(d[p[i] & 15]); } |
| 229 | return s; |
| 230 | } |
| 231 | |
| 232 | static bool read_seed(const std::string &path, std::vector<uint8_t> &seed) { |
| 233 | std::ifstream f(path); |
| 234 | std::string text; |
| 235 | if (!(f >> text) || !from_hex(text, seed) || seed.size() != 32) return false; |
| 236 | return true; |
| 237 | } |
| 238 | |
| 239 | static int safe_action(int argc, char **argv) { |
| 240 | const std::string sub = argc >= 3 ? argv[2] : ""; |
| 241 | auto opt = [&](const char *name, const char *fallback = nullptr) -> std::string { |
| 242 | for (int i = 3; i + 1 < argc; i += 2) if (std::string(argv[i]) == name) return argv[i + 1]; |
| 243 | return fallback ? fallback : ""; |
| 244 | }; |
| 245 | if (sub == "keygen") { |
| 246 | const std::string out = opt("--out"); |
| 247 | if (out.empty()) { std::cerr << "usage: ckey action keygen --out issuer.key\n"; return 2; } |
| 248 | std::ifstream urandom("/dev/urandom", std::ios::binary); |
| 249 | uint8_t seed[32]; |
| 250 | if (!urandom.read(reinterpret_cast<char *>(seed), 32)) { std::cerr << "no system random source\n"; return 1; } |
| 251 | std::ofstream f(out); |
| 252 | f << to_hex(seed, 32) << "\n"; |
| 253 | if (!f) { std::cerr << "could not write " << out << "\n"; return 1; } |
| 254 | uint8_t pk[32]; |
| 255 | ckey_ed25519_public_key(seed, pk); |
| 256 | std::cout << "private seed written to " << out << " (keep it on the issuer's server only)\n" |
| 257 | << "public key (hex): " << to_hex(pk, 32) << "\n"; |
| 258 | return 0; |
| 259 | } |
| 260 | if (sub == "public-key") { |
| 261 | std::vector<uint8_t> seed; |
| 262 | if (!read_seed(opt("--key"), seed)) { std::cerr << "usage: ckey action public-key --key issuer.key\n"; return 2; } |
| 263 | uint8_t pk[32]; |
| 264 | ckey_ed25519_public_key(seed.data(), pk); |
| 265 | std::cout << to_hex(pk, 32) << "\n"; |
| 266 | return 0; |
| 267 | } |
| 268 | if (sub == "sign") { |
| 269 | std::vector<uint8_t> seed; |
| 270 | auto fields = std::make_unique<ckey_safe_action>(); |
| 271 | const std::string issuer = opt("--issuer"), app = opt("--app"), action = opt("--action"), key_id = opt("--key-id"), |
| 272 | request = opt("--request"), scope = opt("--scope", "same-device"), params = opt("--params"), |
| 273 | ttl = opt("--ttl", "120"), now = opt("--now"); |
| 274 | if (!read_seed(opt("--key"), seed) || issuer.empty() || app.empty() || action.empty() || key_id.empty() || request.empty() || |
| 275 | (scope != "same-device" && scope != "cross-device")) { |
| 276 | std::cerr << "usage: ckey action sign --key issuer.key --issuer citeory.com --app com.viadere --action signin.request\n" |
| 277 | " --key-id 2026-11 --request <single-use id> [--scope same-device|cross-device]\n" |
| 278 | " [--params '{\"json\":true}'] [--ttl 120] [--now <unix seconds>]\n"; |
| 279 | return 2; |
| 280 | } |
| 281 | auto copy = [](char *dst, size_t cap, const std::string &v) { if (v.size() >= cap) return false; std::memcpy(dst, v.data(), v.size()); dst[v.size()] = 0; return true; }; |
| 282 | if (!copy(fields->issuer, sizeof(fields->issuer), issuer) || !copy(fields->app, sizeof(fields->app), app) || |
| 283 | !copy(fields->action, sizeof(fields->action), action) || !copy(fields->key_id, sizeof(fields->key_id), key_id) || |
| 284 | !copy(fields->request_id, sizeof(fields->request_id), request) || params.size() > sizeof(fields->params)) { |
| 285 | std::cerr << "a field is too long\n"; |
| 286 | return 2; |
| 287 | } |
| 288 | fields->scope = scope == "cross-device" ? CKEY_SCOPE_CROSS_DEVICE : CKEY_SCOPE_SAME_DEVICE; |
| 289 | fields->issued_at = now.empty() ? static_cast<uint64_t>(std::time(nullptr)) : std::stoull(now); |
| 290 | fields->expires_at = fields->issued_at + std::stoull(ttl); |
| 291 | std::memcpy(fields->params, params.data(), params.size()); |
| 292 | fields->params_len = params.size(); |
| 293 | size_t need = 0; |
| 294 | ckey_diagnostics diag{}; |
| 295 | ckey_safe_action_sign(fields.get(), seed.data(), nullptr, 0, &need, &diag); |
| 296 | std::vector<char> out(need + 1); |
| 297 | if (ckey_safe_action_sign(fields.get(), seed.data(), out.data(), out.size(), &need, &diag) != CKEY_OK) { std::cerr << diag.message << "\n"; return 1; } |
| 298 | std::cout << out.data() << "\n"; |
| 299 | return 0; |
| 300 | } |
| 301 | if (sub == "verify") { |
| 302 | const std::string payload = opt("--payload"), key = opt("--public-key"), now = opt("--now"), max = opt("--max-lifetime", "600"); |
| 303 | std::vector<uint8_t> pk; |
| 304 | if (payload.empty() || !from_hex(key, pk) || pk.size() != 32) { std::cerr << "usage: ckey action verify --payload 'ckey-action:1:...' --public-key <64 hex> [--now <unix seconds>] [--max-lifetime 600]\n"; return 2; } |
| 305 | auto out = std::make_unique<ckey_safe_action>(); |
| 306 | ckey_diagnostics diag{}; |
| 307 | const ckey_status st = ckey_safe_action_verify(reinterpret_cast<const uint8_t *>(payload.data()), payload.size(), pk.data(), |
| 308 | now.empty() ? static_cast<uint64_t>(std::time(nullptr)) : std::stoull(now), |
| 309 | static_cast<uint32_t>(std::stoul(max)), out.get(), &diag); |
| 310 | std::cout << diag.message << "\n"; |
| 311 | if (st == CKEY_OK || st == CKEY_EXPIRED) |
| 312 | std::cout << "issuer " << out->issuer << ", app " << out->app << ", action " << out->action << ", key " << out->key_id |
| 313 | << ", request " << out->request_id << ", " << (out->scope ? "cross-device" : "same-device") << ", valid " << out->issued_at |
| 314 | << ".." << out->expires_at << ", params " << std::string(reinterpret_cast<const char *>(out->params), out->params_len) << "\n"; |
| 315 | return st == CKEY_OK ? 0 : 1; |
| 316 | } |
| 317 | std::cerr << "usage: ckey action (keygen | public-key | sign | verify) ...\n"; |
| 318 | return 2; |
| 319 | } |
| 320 | |
| 321 | int main(int argc, char **argv) { |
| 322 | if (argc >= 2 && std::string(argv[1]) == "encode-scalable") return encode_scalable(argc, argv); |
| 323 | if (argc >= 2 && std::string(argv[1]) == "print-sheet") return print_sheet(argc, argv); |
| 324 | if (argc >= 2 && std::string(argv[1]) == "action") return safe_action(argc, argv); |
| 325 | const bool tierOption = argc == 8 && std::string(argv[2]) == "--tier"; |
| 326 | const int modeIndex = tierOption ? 4 : 2; |
| 327 | const int outIndex = tierOption ? 6 : 4; |
| 328 | if ((argc != 6 && !tierOption) || (std::string(argv[modeIndex]) != "--hex" && std::string(argv[modeIndex]) != "--utf8") || std::string(argv[outIndex]) != "--out") { |
| 329 | std::cerr << "usage:\n" |
| 330 | << " ckey encode-scalable (--utf8 <text> | --hex <hex>) --out <file.svg> [--version 1-31] [--level L|M|Q|H]\n" |
| 331 | << " ckey encode --hex <32-hex-chars> --out <locator.svg>\n" |
| 332 | << " ckey encode-text --utf8 <up-to-200-byte-text> --out <text.svg>\n" |
| 333 | << " ckey encode-text --hex <up-to-400-hex-chars> --out <text.svg>\n" |
| 334 | << " ckey encode-extended --utf8 <up-to-2048-byte-text> --out <extended.svg>\n" |
| 335 | << " ckey encode-extended --tier <256|384|512|768|1024|1536|2048> --utf8 <text> --out <extended.svg>\n" |
| 336 | << " ckey encode-extended --hex <up-to-4096-hex-chars> --out <extended.svg>\n" |
| 337 | << " ckey action (keygen | public-key | sign | verify) ... Safe Actions for issuers\n"; |
| 338 | return 2; |
| 339 | } |
| 340 | const std::string command = argv[1], mode = argv[modeIndex], value = argv[modeIndex+1], output = argv[outIndex+1]; |
| 341 | if (tierOption && command != "encode-extended") { std::cerr << "--tier applies only to encode-extended\n"; return 2; } |
| 342 | ckey_extended_tier requestedTier = CKEY_EXTENDED_TIER_AUTO; |
| 343 | if (tierOption) { |
| 344 | char *end = nullptr; |
| 345 | const long parsed = std::strtol(argv[3], &end, 10); |
| 346 | if (!end || *end != '\0') { std::cerr << "invalid capacity tier\n"; return 2; } |
| 347 | switch (parsed) { |
| 348 | case 256: requestedTier=CKEY_EXTENDED_TIER_256; break; |
| 349 | case 384: requestedTier=CKEY_EXTENDED_TIER_384; break; |
| 350 | case 512: requestedTier=CKEY_EXTENDED_TIER_512; break; |
| 351 | case 768: requestedTier=CKEY_EXTENDED_TIER_768; break; |
| 352 | case 1024: requestedTier=CKEY_EXTENDED_TIER_1024; break; |
| 353 | case 1536: requestedTier=CKEY_EXTENDED_TIER_1536; break; |
| 354 | case 2048: requestedTier=CKEY_EXTENDED_TIER_2048; break; |
| 355 | default: std::cerr << "unsupported capacity tier\n"; return 2; |
| 356 | } |
| 357 | } |
| 358 | ckey_diagnostics diag{}; |
| 359 | std::vector<uint8_t> payload; |
| 360 | if (mode == "--hex") { |
| 361 | if (!from_hex(value, payload)) { std::cerr << "hex input must be an even number of hexadecimal digits\n"; return 2; } |
| 362 | } else payload.assign(value.begin(), value.end()); |
| 363 | if (command == "encode") { |
| 364 | if (mode != "--hex" || payload.size() != CKEY_LOCATOR_PAYLOAD_BYTES) { std::cerr << "locator input must be exactly 16 bytes encoded as 32 hexadecimal characters\n"; return 2; } |
| 365 | ckey_symbol symbol{}; |
| 366 | if (ckey_encode_locator(payload.data(), payload.size(), &symbol, &diag) != CKEY_OK) { std::cerr << diag.message << '\n'; return 1; } |
| 367 | size_t needed = 0; ckey_render_svg(&symbol, 16, nullptr, 0, &needed); |
| 368 | std::string svg(needed, '\0'); |
| 369 | if (ckey_render_svg(&symbol, 16, svg.data(), svg.size(), &needed) != CKEY_OK || !write_svg(output, svg)) { std::cerr << "SVG rendering failed\n"; return 1; } |
| 370 | std::cout << diag.message << "; profile=1; format=0." << CKEY_FORMAT_VERSION << ".0; payload-bytes=16; svg=" << output << '\n'; |
| 371 | return 0; |
| 372 | } |
| 373 | if (command == "encode-text") { |
| 374 | if (payload.empty() || payload.size() > CKEY_TEXT_PAYLOAD_MAX_BYTES) { std::cerr << "text input must contain 1 to 200 UTF-8 bytes\n"; return 2; } |
| 375 | ckey_text_symbol symbol{}; |
| 376 | if (ckey_encode_text(payload.data(), payload.size(), &symbol, &diag) != CKEY_OK) { std::cerr << diag.message << '\n'; return 1; } |
| 377 | size_t needed = 0; ckey_render_text_svg(&symbol, 16, nullptr, 0, &needed); |
| 378 | std::string svg(needed, '\0'); |
| 379 | if (ckey_render_text_svg(&symbol, 16, svg.data(), svg.size(), &needed) != CKEY_OK || !write_svg(output, svg)) { std::cerr << "SVG rendering failed\n"; return 1; } |
| 380 | std::cout << diag.message << "; profile=2; format=0." << CKEY_TEXT_FORMAT_VERSION << ".0; payload-bytes=" << payload.size() << "; codeword-bytes=255; svg=" << output << '\n'; |
| 381 | return 0; |
| 382 | } |
| 383 | if (command == "encode-extended") { |
| 384 | if (payload.empty() || payload.size() > CKEY_EXTENDED_PAYLOAD_MAX_BYTES) { std::cerr << "extended input must contain 1 to 2048 bytes\n"; return 2; } |
| 385 | ckey_extended_symbol symbol{}; |
| 386 | const ckey_status encoded = ckey_encode_extended_tiered(payload.data(), payload.size(), requestedTier, &symbol, &diag); |
| 387 | if (encoded != CKEY_OK) { std::cerr << diag.message << '\n'; return 1; } |
| 388 | size_t needed = 0; ckey_render_extended_svg(&symbol, 16, nullptr, 0, &needed); |
| 389 | std::string svg(needed, '\0'); |
| 390 | if (ckey_render_extended_svg(&symbol, 16, svg.data(), svg.size(), &needed) != CKEY_OK || !write_svg(output, svg)) { std::cerr << "extended SVG rendering failed\n"; return 1; } |
| 391 | std::cout << diag.message << "; profile=3; format=0." << CKEY_EXTENDED_FORMAT_VERSION << ".0; payload-bytes=" << payload.size() << "; codeword-bytes=" << CKEY_EXTENDED_CODEWORD_BYTES << "; svg=" << output << '\n'; |
| 392 | return 0; |
| 393 | } |
| 394 | std::cerr << "unknown command\n"; return 2; |
| 395 | } |