CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
GpgModule.cpp
Go to the documentation of this file.
1#include "mod_gpg/GpgModule.h"
7#include "openpgp/xsig.h"
8#include "openpgp/rsa.h"
9#include "cdc_msg/MessageTransfer.h"
11#include "cdc_core/Raii.h"
13#include "cdc_core/UsbManager.h"
14#include "cdc_ui/I18n.h"
17#include "cdc_ui/ViewStack.h"
18#include "cdc_views/ListView.h"
20#include "cdc_views/InfoView.h"
23#include "cdc_views/ToastView.h"
25#include "cdc_core/PinManager.h"
30#include "serial_cmd/Console.h"
31#include "mod_gpg/gpg.h"
32#include "cdc_log.h"
33#include "esp_timer.h"
34#include "esp_random.h"
35#include "esp_attr.h"
36#include <new>
37#include <cstring>
38#include <cstdio>
39#include <cctype>
40
41static const char* TAG = "GPG";
42
43namespace cdc::mod_gpg {
44
45constexpr ui::I18nEntry kStrings[] = {
46 {"mod_gpg.title", "GPG"},
47 {"mod_gpg.generate", "Generate Keys"},
48 {"mod_gpg.export", "Export Public"},
49 {"mod_gpg.reset", "Reset"},
50 {"mod_gpg.settings", "Settings"},
51 {"mod_gpg.user_pin", "User PIN"},
52 {"mod_gpg.admin_pin", "Admin PIN"},
53 {"mod_gpg.pin_locked", "Locked"},
54 {"mod_gpg.slot_error", "Slot map error"},
55 {"mod_gpg.name", "Name"},
56 {"mod_gpg.email", "Email (optional)"},
57 {"mod_gpg.curve", "Curve"},
58 {"mod_gpg.curve_ed25519", "Ed25519"},
59 {"mod_gpg.curve_p256", "P-256"},
60 {"mod_gpg.no_key", "No key configured"},
61 {"mod_gpg.confirm_reset", "Reset all GPG keys?"},
62 {"mod_gpg.export_title", "GPG Public Key"},
63 {"mod_gpg.send", "Send Key"},
64 {"mod_gpg.received", "Received Keys"},
65 {"mod_gpg.recv_none", "No received keys"},
66 {"mod_gpg.recv_title", "Received Keys"},
67 {"mod_gpg.recv_details", "Key Details"},
68 {"mod_gpg.recv_sign", "Cross-Sign"},
69 {"mod_gpg.recv_export", "Export"},
70 {"mod_gpg.recv_forward", "Forward"},
71 {"mod_gpg.recv_qr", "Show QR"},
72 {"mod_gpg.recv_confirm_sign","Sign this key?"},
73 {"mod_gpg.recv_confirm_del", "Delete this key?"},
74 {"mod_gpg.recv_signed", "Signed"},
75 {"mod_gpg.recv_unsigned", "Unsigned"},
76 {"mod_gpg.recv_export_title","Key Export"},
77 {"mod_gpg.recv_send_sig", "Send Signature"},
78 {"mod_gpg.toast_signed", "Cross-signed"},
79 {"mod_gpg.toast_sign_fail", "Sign failed"},
80 {"mod_gpg.cert_received", "Incoming key certification"},
81 {"mod_gpg.mycerts", "My Certifications"},
82 {"mod_gpg.mycerts_none", "No certifications"},
83 {"mod_gpg.mycerts_details", "Certification"},
84 {"mod_gpg.mycerts_confirm_del","Delete this certification?"},
85};
86
90
92static constexpr const char* kGpgKeyMime = "application/pgp-keys";
93
100static bool deliverGpgKey(const uint8_t* data, uint32_t len,
101 const char* /*mime*/, const char* /*peerName*/) {
102 gpg_recv_key_t key = {};
103 if (!gpgParseKeyPayload(data, static_cast<size_t>(len), &key)) {
104 LOG_W(TAG, "Rejecting malformed GPG key payload (%u bytes)",
105 static_cast<unsigned>(len));
106 return false;
107 }
108 if (!GpgRecvStore::instance().addKey(key)) {
109 LOG_W(TAG, "GPG received-key store full");
110 return false;
111 }
112 LOG_I(TAG, "Received GPG key from %s", key.user_id);
113 return true;
114}
115
117static constexpr const char* kGpgCertMime = "application/pgp-signature";
118
126static bool deliverGpgCert(const uint8_t* data, uint32_t len,
127 const char* /*mime*/, const char* /*peerName*/) {
128 gpg_self_cert_t cert = {};
129 uint8_t target_fp[20] = {0};
130 if (!gpgParseCertPayload(data, static_cast<size_t>(len), &cert, target_fp)) {
131 LOG_W(TAG, "Rejecting malformed GPG cert payload (%u bytes)",
132 static_cast<unsigned>(len));
133 return false;
134 }
135 gpg_status_t status = {};
136 if (!gpg_get_status(&status)) return false;
137 if (std::memcmp(target_fp, status.fingerprint, 20) != 0) {
138 LOG_W(TAG, "Rejecting cert: targets a different key");
139 return false;
140 }
141 if (!GpgSelfCertStore::instance().addCert(cert)) {
142 LOG_W(TAG, "GPG self-cert store full");
143 return false;
144 }
145 LOG_I(TAG, "Stored certification from %s", cert.issuer_uid);
146 return true;
147}
148
149static constexpr const char* CMD_MODULE = "gpg";
150static bool s_commandsRegistered = false;
151
152static void cmd_gpg_status(const char* args);
153static void cmd_gpg_generate(const char* args);
154static void cmd_gpg_export(const char* args);
155static void cmd_gpg_reset(const char* args);
156static void cmd_gpg_recv_list(const char* args);
157static void cmd_gpg_recv_info(const char* args);
158static void cmd_gpg_recv_delete(const char* args);
159static void cmd_gpg_recv_cross_sign(const char* args);
160static void cmd_gpg_recv_export(const char* args);
161static void cmd_gpg_mycert_list(const char* args);
162static void cmd_gpg_mycert_delete(const char* args);
163static void cmd_gpg_recv_import(const char* args);
164static void cmd_gpg_mycert_import(const char* args);
165static void cmd_gpg_rsa_selftest(const char* args);
166
168 {"STATUS", "", "Show keys, fingerprints, counters", cmd_gpg_status},
169 {"GENERATE", "<curve> <user_id>", "Generate SIG+DEC+AUT keys (curve 1=Ed25519, 2=P-256)", cmd_gpg_generate},
170 {"EXPORT", "", "Print own OpenPGP public key (armored, with certifications)", cmd_gpg_export},
171 {"RESET", "[token]", "Two-step destructive reset of all GPG keys", cmd_gpg_reset},
172 {"RECV_LIST", "", "List public keys received from peers", cmd_gpg_recv_list},
173 {"RECV_INFO", "<index>", "Show a received peer key's details", cmd_gpg_recv_info},
174 {"RECV_DELETE", "<index>", "Delete a received peer key", cmd_gpg_recv_delete},
175 {"RECV_IMPORT", "<hex>", "Import a peer key wire payload (hex) into the received store", cmd_gpg_recv_import},
176 {"RECV_CROSS_SIGN", "<index>", "Cross-sign a received peer key with our signature key", cmd_gpg_recv_cross_sign},
177 {"RECV_EXPORT", "<index>", "Export a received peer key (armored, encryptable; adds our cross-signature if present)", cmd_gpg_recv_export},
178 {"MYCERT_LIST", "", "List third-party certifications collected on the own key", cmd_gpg_mycert_list},
179 {"MYCERT_DELETE", "<index>", "Delete a stored certification on the own key", cmd_gpg_mycert_delete},
180 {"MYCERT_IMPORT", "<hex>", "Import a certification return payload (hex) onto the own key", cmd_gpg_mycert_import},
181 {"RSA_SELFTEST", "[bits]", "Software RSA self-test (gen/sign/verify/decrypt), default 2048", cmd_gpg_rsa_selftest},
182 {nullptr, nullptr, nullptr, nullptr},
183};
184
185static void cmd_gpg(const char* args) {
187}
188
192static void registerCommands() {
193 if (s_commandsRegistered) return;
195 auto& registry = cdc::serial::getCommandRegistry();
196 registry.registerCommand({"GPG",
197 "GPG card: STATUS/GENERATE/EXPORT/RESET",
198 cmd_gpg, CMD_MODULE, true, kGpgSubs});
199}
200
205static void cmd_gpg_status(const char* args) {
206 (void)args;
207 gpg_status_t status = {};
208 if (!gpg_get_status(&status)) {
209 cdc::serial::Console::printf("ERROR: No key configured\r\n");
210 return;
211 }
212 cdc::serial::Console::printf("User-ID: %s\r\n", status.user_id);
213 cdc::serial::Console::printf("Curve: %s\r\n",
214 status.curve == CDC_CURVE_ED25519 ? "Ed25519" : "P-256");
215 cdc::serial::Console::printf("Created: %lu\r\n", static_cast<unsigned long>(status.created_at));
216 cdc::serial::Console::printf("Sign Count: %lu\r\n", static_cast<unsigned long>(status.sign_count));
217}
218
223static void cmd_gpg_generate(const char* args) {
224 char curveBuf[8] = {};
225 char userId[GPG_USER_ID_MAX] = {};
226
227 const char* p = args;
228 while (p && *p && std::isspace(static_cast<unsigned char>(*p))) p++;
229 if (!p || !*p) {
230 cdc::serial::Console::printf("Usage: GPG GENERATE <curve> <user_id>\r\n");
231 return;
232 }
233
234 size_t i = 0;
235 while (p[i] && !std::isspace(static_cast<unsigned char>(p[i])) && i + 1 < sizeof(curveBuf)) {
236 curveBuf[i] = p[i];
237 i++;
238 }
239 curveBuf[i] = '\0';
240 p += i;
241 while (p && *p && std::isspace(static_cast<unsigned char>(*p))) p++;
242 if (!p || !*p) {
243 cdc::serial::Console::printf("Usage: GPG GENERATE <curve> <user_id>\r\n");
244 return;
245 }
246 strncpy(userId, p, sizeof(userId) - 1);
247
248 uint8_t curve = (atoi(curveBuf) == 2) ? CDC_CURVE_P256 : CDC_CURVE_ED25519;
250 bool ok = gpg_generate_key(curve);
251 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
252}
253
258static void cmd_gpg_export(const char* args) {
259 (void)args;
260 auto buf = ::cdc::core::psramAlloc<char>(4096);
261 if (!buf) {
262 cdc::serial::Console::printf("ERROR: out of memory\r\n");
263 return;
264 }
265 size_t out_len = 0;
266 if (!gpgBuildOwnSignedKeyArmored(buf.get(), 4096, &out_len)) {
267 cdc::serial::Console::printf("ERROR\r\n");
268 return;
269 }
270 cdc::serial::Console::printf("%s\r\n", buf.get());
271}
272
277static char s_reset_token[7] = {};
278static uint64_t s_reset_token_ts_us = 0;
279static constexpr uint64_t RESET_TOKEN_TIMEOUT_US = 30ULL * 1000ULL * 1000ULL;
280
281static void cmd_gpg_reset(const char* args) {
282 const uint64_t now = static_cast<uint64_t>(esp_timer_get_time());
283 const bool token_active = (s_reset_token[0] != '\0') &&
285
286 if (args && *args && token_active && strcmp(args, s_reset_token) == 0) {
287 memset(s_reset_token, 0, sizeof(s_reset_token));
289 bool ok = gpg_reset();
290 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
291 return;
292 }
293
294 uint8_t r[3];
295 esp_fill_random(r, sizeof(r));
296 snprintf(s_reset_token, sizeof(s_reset_token), "%02X%02X%02X", r[0], r[1], r[2]);
299 "WARNING: this wipes ALL GPG keys (SIG/DEC/AUT), the DEC backup, and PINs.\r\n"
300 "Confirm within 30s: GPG RESET %s\r\n",
302}
303
304static void fp_to_hex(const uint8_t* fp, size_t len, char* out, size_t out_size) {
305 if (out_size < len * 2 + 1) {
306 if (out_size > 0) out[0] = '\0';
307 return;
308 }
309 for (size_t i = 0; i < len; ++i) {
310 snprintf(out + i * 2, 3, "%02X", fp[i]);
311 }
312 out[len * 2] = '\0';
313}
314
315static void cmd_gpg_recv_list(const char* args) {
316 (void)args;
317 auto& store = GpgRecvStore::instance();
319 if (!buf) {
320 cdc::serial::Console::printf("ERROR: out of memory\r\n");
321 return;
322 }
323 uint8_t n = store.listIndex(buf.get(), GpgRecvStore::kMaxKeys);
324 cdc::serial::Console::printf("OK: %u received keys\r\n", static_cast<unsigned>(n));
325 for (uint8_t i = 0; i < n; ++i) {
326 gpg_recv_key_t key = {};
327 if (!store.getKey(i, &key)) continue;
328 char fp_short[9];
329 fp_to_hex(key.fingerprint_v4, 4, fp_short, sizeof(fp_short));
330 cdc::serial::Console::printf("[%u] %s\r\n", i, key.user_id);
331 cdc::serial::Console::printf(" FP: %s...\r\n", fp_short);
332 cdc::serial::Console::printf(" Signed: %s\r\n",
333 key.sig_len > 0 ? "Yes" : "No");
334 }
335}
336
337static bool parse_index(const char* args, uint8_t* out) {
338 if (!args || !*args) return false;
339 char* end = nullptr;
340 long v = strtol(args, &end, 10);
341 if (end == args || v < 0 || v > 255) return false;
342 *out = static_cast<uint8_t>(v);
343 return true;
344}
345
346static void cmd_gpg_recv_info(const char* args) {
347 uint8_t idx = 0;
348 if (!parse_index(args, &idx)) {
349 cdc::serial::Console::printf("Usage: GPG RECV_INFO <index>\r\n");
350 return;
351 }
352 gpg_recv_key_t key = {};
353 if (!GpgRecvStore::instance().getKey(idx, &key)) {
354 cdc::serial::Console::printf("ERROR: index not found\r\n");
355 return;
356 }
357 char fp_v4[41];
358 char fp_v5[65];
359 fp_to_hex(key.fingerprint_v4, 20, fp_v4, sizeof(fp_v4));
360 fp_to_hex(key.fingerprint_v5, 32, fp_v5, sizeof(fp_v5));
361
362 char ts_buf[32];
363 time_t t = static_cast<time_t>(key.received_at);
364 struct tm tm_v;
365 gmtime_r(&t, &tm_v);
366 strftime(ts_buf, sizeof(ts_buf), "%Y-%m-%d %H:%M:%S UTC", &tm_v);
367
368 cdc::serial::Console::printf("OK: Key details\r\n");
369 cdc::serial::Console::printf("User-ID: %s\r\n", key.user_id);
370 cdc::serial::Console::printf("Curve: %s\r\n",
371 key.curve == CDC_CURVE_ED25519 ? "Ed25519" : "P-256");
372 cdc::serial::Console::printf("Fingerprint V4: %s\r\n", fp_v4);
373 cdc::serial::Console::printf("Fingerprint V5: %s\r\n", fp_v5);
374 cdc::serial::Console::printf("Received: %s\r\n", ts_buf);
375 cdc::serial::Console::printf("Signed: %s\r\n", key.sig_len > 0 ? "Yes" : "No");
376 cdc::serial::Console::printf("Verified: %s\r\n",
377 (key.flags & kGpgRecvFlagVerified) ? "Yes" : "No");
378 if (key.sig_len > 0) {
379 char sig_hex[2 * 64 + 1];
380 fp_to_hex(key.my_signature, key.sig_len, sig_hex, sizeof(sig_hex));
381 cdc::serial::Console::printf("Signature: %s\r\n", sig_hex);
382 }
383}
384
385static void cmd_gpg_recv_delete(const char* args) {
386 uint8_t idx = 0;
387 if (!parse_index(args, &idx)) {
388 cdc::serial::Console::printf("Usage: GPG RECV_DELETE <index>\r\n");
389 return;
390 }
392 ? "OK\r\n"
393 : "ERROR: delete failed\r\n");
394}
395
396static void cmd_gpg_recv_cross_sign(const char* args) {
397 uint8_t idx = 0;
398 if (!parse_index(args, &idx)) {
399 cdc::serial::Console::printf("Usage: GPG RECV_CROSS_SIGN <index>\r\n");
400 return;
401 }
402 gpg_recv_key_t key = {};
403 if (!GpgRecvStore::instance().getKey(idx, &key)) {
404 cdc::serial::Console::printf("ERROR: index not found\r\n");
405 return;
406 }
407 uint32_t now = static_cast<uint32_t>(time(nullptr));
408 uint8_t sig[64] = {0};
409 if (!gpgCrossSign(key, now, sig)) {
410 cdc::serial::Console::printf("ERROR: signing failed\r\n");
411 return;
412 }
413 if (!GpgRecvStore::instance().setSignature(idx, sig, 64, now,
415 cdc::serial::Console::printf("ERROR: store update failed\r\n");
416 return;
417 }
419}
420
421static void cmd_gpg_recv_export(const char* args) {
422 uint8_t idx = 0;
423 if (!parse_index(args, &idx)) {
424 cdc::serial::Console::printf("Usage: GPG RECV_EXPORT <index>\r\n");
425 return;
426 }
427 gpg_recv_key_t key = {};
428 if (!GpgRecvStore::instance().getKey(idx, &key)) {
429 cdc::serial::Console::printf("ERROR: index not found\r\n");
430 return;
431 }
432
433 auto buf = ::cdc::core::psramAlloc<char>(4096);
434 if (!buf) {
435 cdc::serial::Console::printf("ERROR: out of memory\r\n");
436 return;
437 }
438 size_t out_len = 0;
439 if (!gpgBuildReceivedKeyArmored(key, buf.get(), 4096, &out_len)) {
440 cdc::serial::Console::printf("ERROR: export failed\r\n");
441 return;
442 }
443 // print() streams the full buffer; printf() would clip it to vprintf's
444 // 256-byte stack buffer and drop the armor's CRC and END line.
446}
447
448
449static void cmd_gpg_mycert_list(const char* args) {
450 (void)args;
451 auto& store = GpgSelfCertStore::instance();
453 if (!buf) {
454 cdc::serial::Console::printf("ERROR: out of memory\r\n");
455 return;
456 }
457 uint8_t n = store.listIndex(buf.get(), GpgSelfCertStore::kMaxCerts);
458 cdc::serial::Console::printf("OK: %u certifications\r\n", static_cast<unsigned>(n));
459 for (uint8_t i = 0; i < n; ++i) {
460 gpg_self_cert_t cert = {};
461 if (!store.getCert(i, &cert)) continue;
462 char fp_short[9];
463 fp_to_hex(cert.issuer_fp_v4, 4, fp_short, sizeof(fp_short));
464 cdc::serial::Console::printf("[%u] %s (issuer FP %s...)\r\n",
465 i, cert.issuer_uid, fp_short);
466 }
467}
468
469static void cmd_gpg_mycert_delete(const char* args) {
470 uint8_t idx = 0;
471 if (!parse_index(args, &idx)) {
472 cdc::serial::Console::printf("Usage: GPG MYCERT_DELETE <index>\r\n");
473 return;
474 }
476 ? "OK\r\n"
477 : "ERROR: delete failed\r\n");
478}
479
481static bool parse_hex(const char* args, uint8_t* out, size_t out_cap, size_t* out_len) {
482 if (!args || !out || !out_len) return false;
483 size_t n = 0;
484 int hi = -1;
485 for (const char* p = args; *p; ++p) {
486 char c = *p;
487 if (std::isspace(static_cast<unsigned char>(c))) continue;
488 int v;
489 if (c >= '0' && c <= '9') v = c - '0';
490 else if (c >= 'a' && c <= 'f') v = c - 'a' + 10;
491 else if (c >= 'A' && c <= 'F') v = c - 'A' + 10;
492 else return false;
493 if (hi < 0) {
494 hi = v;
495 } else {
496 if (n >= out_cap) return false;
497 out[n++] = static_cast<uint8_t>((hi << 4) | v);
498 hi = -1;
499 }
500 }
501 if (hi >= 0) return false; // odd number of hex digits
502 *out_len = n;
503 return n > 0;
504}
505
506static void cmd_gpg_recv_import(const char* args) {
507 static EXT_RAM_BSS_ATTR uint8_t payload[kGpgKeyPayloadMax];
508 size_t len = 0;
509 if (!parse_hex(args, payload, sizeof(payload), &len)) {
510 cdc::serial::Console::printf("Usage: GPG RECV_IMPORT <hex>\r\n");
511 return;
512 }
513 gpg_recv_key_t key = {};
514 if (!gpgParseKeyPayload(payload, len, &key)) {
515 cdc::serial::Console::printf("ERROR: malformed key payload\r\n");
516 return;
517 }
519 ? "OK\r\n"
520 : "ERROR: store full\r\n");
521}
522
523static void cmd_gpg_mycert_import(const char* args) {
524 static EXT_RAM_BSS_ATTR uint8_t payload[kGpgCertPayloadMax];
525 size_t len = 0;
526 if (!parse_hex(args, payload, sizeof(payload), &len)) {
527 cdc::serial::Console::printf("Usage: GPG MYCERT_IMPORT <hex>\r\n");
528 return;
529 }
530 gpg_self_cert_t cert = {};
531 uint8_t target_fp[20] = {0};
532 if (!gpgParseCertPayload(payload, len, &cert, target_fp)) {
533 cdc::serial::Console::printf("ERROR: malformed cert payload\r\n");
534 return;
535 }
536 gpg_status_t status = {};
537 if (!gpg_get_status(&status)) {
538 cdc::serial::Console::printf("ERROR: no own key\r\n");
539 return;
540 }
541 if (std::memcmp(target_fp, status.fingerprint, 20) != 0) {
542 cdc::serial::Console::printf("ERROR: cert targets a different key\r\n");
543 return;
544 }
546 ? "OK\r\n"
547 : "ERROR: store full\r\n");
548}
549
555static void cmd_gpg_rsa_selftest(const char* args) {
556 uint16_t bits = 2048;
557 while (args && *args && std::isspace(static_cast<unsigned char>(*args))) args++;
558 if (args && *args) {
559 int v = atoi(args);
560 if (v == 2048 || v == 3072 || v == 4096) {
561 bits = static_cast<uint16_t>(v);
562 } else {
563 cdc::serial::Console::printf("Usage: GPG RSA_SELFTEST [2048|3072|4096]\r\n");
564 return;
565 }
566 }
567 cdc::serial::Console::printf("RSA-%u self-test running (may take a while)...\r\n", bits);
568 bool ok = gpg_rsa_selftest(bits);
569 cdc::serial::Console::printf("RSA_SELFTEST %u: %s\r\n", bits, ok ? "OK" : "FAIL");
570}
571
579static bool s_viewsInitialized = false;
582 { nullptr, 0, false, nullptr },
583 { nullptr, 0, false, nullptr },
584};
585
587 char name[64];
588 char email[64];
589 uint8_t curve;
590};
591
592EXT_RAM_BSS_ATTR static WizardState s_wizard = {};
593
604
605static void showStatus();
606static void wizardStart();
607static void showExport();
608static void confirmReset();
609static void showSettings();
610static void onSettingsSelect(uint16_t index, void*);
611static void showSendKey();
612static void showReceivedKeys();
613static void showMyCertifications();
614
618static void onMenuSelect(uint16_t, void* userData) {
619 switch (static_cast<GpgMenuAction>(reinterpret_cast<uintptr_t>(userData))) {
620 case GPG_MENU_STATUS: showStatus(); break;
621 case GPG_MENU_GENERATE: wizardStart(); break;
622 case GPG_MENU_EXPORT: showExport(); break;
623 case GPG_MENU_SEND: showSendKey(); break;
624 case GPG_MENU_RECEIVED: showReceivedKeys(); break;
626 case GPG_MENU_SETTINGS: showSettings(); break;
627 case GPG_MENU_RESET: confirmReset(); break;
628 default: break;
629 }
630}
631
632static inline ui::ListItem makeMenuItem(const char* label, GpgMenuAction action) {
633 return { label, 0, false, reinterpret_cast<void*>(static_cast<uintptr_t>(action)) };
634}
635
639static void rebuildMenu() {
640 const bool hasKeys = openpgp_has_any_key();
641 uint8_t count = 0;
642 s_menuItems[count++] = makeMenuItem(ui::tr("core.status"), GPG_MENU_STATUS);
643 if (!hasKeys) {
644 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.generate"), GPG_MENU_GENERATE);
645 } else {
646 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.export"), GPG_MENU_EXPORT);
647 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.send"), GPG_MENU_SEND);
648 }
649 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.received"), GPG_MENU_RECEIVED);
650 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.mycerts"), GPG_MENU_MYCERTS);
651 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.settings"), GPG_MENU_SETTINGS);
652 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.reset"), GPG_MENU_RESET);
653 s_menuView.init(ui::tr("mod_gpg.title"), s_menuItems, count);
654}
655
661static bool gpg_verify_pw1(const char* pin) {
663}
664
670static bool gpg_verify_pw3(const char* pin) {
672}
673
680static bool gpg_change_pw1(const char*, const char* newPin) {
681 return pin_storage_openpgp_change_pw1(newPin);
682}
683
690static bool gpg_change_pw3(const char*, const char* newPin) {
691 return pin_storage_openpgp_change_pw3(newPin);
692}
693
698static uint8_t gpg_retries_pw1() {
700}
701
706static uint8_t gpg_retries_pw3() {
708}
709
714static bool gpg_blocked_pw1() {
716}
717
722static bool gpg_blocked_pw3() {
724}
725
730static void onGpgPinComplete(bool) {
732}
733
739static void onSettingsSelect(uint16_t index, void*) {
740 s_pinChangeView.setOnComplete(onGpgPinComplete);
741 s_pinChangeView.setTitle(index == 0 ? ui::tr("mod_gpg.user_pin") : ui::tr("mod_gpg.admin_pin"));
742 if (index == 0) {
743 s_pinChangeView.setVerifyCallback(gpg_verify_pw1);
744 s_pinChangeView.setChangeCallback(gpg_change_pw1);
745 s_pinChangeView.setRetriesCallback(gpg_retries_pw1);
746 s_pinChangeView.setBlockedCallback(gpg_blocked_pw1);
748 } else {
749 s_pinChangeView.setVerifyCallback(gpg_verify_pw3);
750 s_pinChangeView.setChangeCallback(gpg_change_pw3);
751 s_pinChangeView.setRetriesCallback(gpg_retries_pw3);
752 s_pinChangeView.setBlockedCallback(gpg_blocked_pw3);
754 }
756}
757
758static char s_userPinLabel[48];
759static char s_adminPinLabel[48];
760
769static void formatPinLabel(char* out, size_t outLen, const char* base,
770 bool blocked, uint8_t retries) {
771 if (blocked) {
772 snprintf(out, outLen, "%s [%s]", base, ui::tr("mod_gpg.pin_locked"));
773 } else {
774 snprintf(out, outLen, "%s (%u)", base, static_cast<unsigned>(retries));
775 }
776}
777
781static void showSettings() {
782 formatPinLabel(s_userPinLabel, sizeof(s_userPinLabel), ui::tr("mod_gpg.user_pin"),
784 formatPinLabel(s_adminPinLabel, sizeof(s_adminPinLabel), ui::tr("mod_gpg.admin_pin"),
788 s_settingsView.init(ui::tr("mod_gpg.settings"), s_settingsItems, 2);
789 s_settingsView.setOnSelect(onSettingsSelect);
791}
792
796static void showStatus() {
797 gpg_status_t status = {};
798 if (!gpg_get_status(&status)) {
799 s_infoView.init(ui::tr("core.status"), ui::tr("mod_gpg.no_key"));
801 return;
802 }
803
804 char fp_hex[GPG_FINGERPRINT_LEN * 2 + 1] = {};
805 for (size_t i = 0; i < GPG_FINGERPRINT_LEN; i++) {
806 snprintf(fp_hex + i * 2, 3, "%02X", status.fingerprint[i]);
807 }
808 const char* curveName = status.curve == CDC_CURVE_ED25519 ? ui::tr("mod_gpg.curve_ed25519")
809 : ui::tr("mod_gpg.curve_p256");
810 static char detail[512];
811 snprintf(detail, sizeof(detail),
812 "User-ID: %s\nCurve: %s\nFP: %s\nCreated: %lu\nSign Count: %lu",
813 status.user_id, curveName, fp_hex,
814 static_cast<unsigned long>(status.created_at),
815 static_cast<unsigned long>(status.sign_count));
816 s_infoView.init(ui::tr("core.status"), detail);
818}
819
820static void onWizardName(const char* text);
821static void onWizardEmail(const char* text);
822static void onWizardCurve(uint16_t index, void*);
823
827static void wizardStart() {
828 memset(&s_wizard, 0, sizeof(s_wizard));
829 s_t9Input.init(ui::tr("mod_gpg.name"), nullptr, 63);
830 s_t9Input.setOnSave(onWizardName);
832}
833
838static void onWizardName(const char* text) {
839 strncpy(s_wizard.name, text ? text : "", sizeof(s_wizard.name) - 1);
840 s_t9Input.init(ui::tr("mod_gpg.email"), nullptr, 63);
841 s_t9Input.setOnSave(onWizardEmail);
843}
844
849static void onWizardEmail(const char* text) {
850 strncpy(s_wizard.email, text ? text : "", sizeof(s_wizard.email) - 1);
851 static ui::ListItem curveItems[] = {
852 { nullptr, 0, false, nullptr },
853 { nullptr, 0, false, nullptr },
854 };
855 curveItems[0].label = ui::tr("mod_gpg.curve_ed25519");
856 curveItems[1].label = ui::tr("mod_gpg.curve_p256");
857 s_curveView.init(ui::tr("mod_gpg.curve"), curveItems, 2);
858 s_curveView.setOnSelect(onWizardCurve);
860}
861
867static void onWizardCurve(uint16_t index, void*) {
868 s_wizard.curve = (index == 0) ? CDC_CURVE_ED25519 : CDC_CURVE_P256;
869 char user_id[GPG_USER_ID_MAX] = {};
870 size_t name_len = strnlen(s_wizard.name, sizeof(s_wizard.name) - 1);
871 size_t email_len = strnlen(s_wizard.email, sizeof(s_wizard.email) - 1);
872 if (email_len == 0) {
873 snprintf(user_id, sizeof(user_id), "%.*s", static_cast<int>(sizeof(user_id) - 1), s_wizard.name);
874 } else {
875 size_t max_len = sizeof(user_id) - 1;
876 size_t name_fit = name_len > max_len ? max_len : name_len;
877 size_t email_fit = 0;
878 if (name_fit < max_len) {
879 size_t remaining = max_len - name_fit;
880 if (remaining > 3) {
881 email_fit = remaining - 3;
882 }
883 }
884 if (email_fit == 0) {
885 memcpy(user_id, s_wizard.name, name_fit);
886 user_id[name_fit] = '\0';
887 } else {
888 size_t pos = 0;
889 memcpy(user_id + pos, s_wizard.name, name_fit);
890 pos += name_fit;
891 user_id[pos++] = ' ';
892 user_id[pos++] = '<';
893 memcpy(user_id + pos, s_wizard.email, email_fit);
894 pos += email_fit;
895 user_id[pos++] = '>';
896 user_id[pos] = '\0';
897 }
898 }
900 if (gpg_generate_key(s_wizard.curve)) {
901 ui::showToastSuccess(ui::tr("core.ok"));
902 } else {
903 ui::showToastError(ui::tr("core.failed"));
904 }
906}
907
911static void showExport() {
912 EXT_RAM_BSS_ATTR static char armored[4096];
913 static char title_buf[GPG_USER_ID_MAX + 8];
914 static char detail_buf[160];
915 size_t out_len = 0;
916 if (!gpgBuildOwnSignedKeyArmored(armored, sizeof(armored), &out_len)) {
917 ui::showToastError(ui::tr("core.failed"));
918 return;
919 }
922
923 gpg_status_t status = {};
924 title_buf[0] = '\0';
925 detail_buf[0] = '\0';
926 if (gpg_get_status(&status) && status.initialized) {
927 strlcpy(title_buf,
928 status.user_id[0] ? status.user_id : "(card)",
929 sizeof(title_buf));
930
931 char alchemy[KEY_FINGERPRINT_MAX_LEN] = {};
932 gpg_alchemy_fingerprint(alchemy, sizeof(alchemy));
933 const char* curveName = (status.curve == CDC_CURVE_ED25519) ? "Ed25519" : "P-256";
934 snprintf(detail_buf, sizeof(detail_buf), "%s\n%s", curveName, alchemy);
935 }
936
937 s_qrView.init(armored,
938 title_buf[0] ? title_buf : nullptr,
939 detail_buf[0] ? detail_buf : nullptr);
941}
942
947static void onResetConfirm(void*) {
948 if (gpg_reset()) {
949 ui::showToastSuccess(ui::tr("core.ok"));
950 } else {
951 ui::showToastError(ui::tr("core.failed"));
952 }
953}
954
958static void confirmReset() {
959 ui::showConfirm(ui::tr("mod_gpg.confirm_reset"), onResetConfirm, nullptr,
961}
962
970EXT_RAM_BSS_ATTR static char s_recvListLabels[GpgRecvStore::kMaxKeys][72];
971static uint8_t s_recvSelectedIndex = 0;
973EXT_RAM_BSS_ATTR static char s_recvDetailText[640];
974EXT_RAM_BSS_ATTR static char s_recvExportBuf[4096];
975
976static void rebuildReceivedList();
977static void onReceivedListSelect(uint16_t index, void*);
978static void showReceivedDetail();
979static void onReceivedActionSelect(uint16_t index, void*);
980static void onReceivedSignConfirm(void*);
981static void onReceivedDeleteConfirm(void*);
982
983static void showSendKey() {
984 // Push our own public key to a nearby badge; the framework owns the peer
985 // picker, consent, encryption and progress UI.
986 static EXT_RAM_BSS_ATTR uint8_t payload[kGpgKeyPayloadMax];
987 size_t len = gpgBuildOwnKeyPayload(payload, sizeof(payload));
988 if (len == 0) {
989 ui::showToastInfo(ui::tr("mod_gpg.no_key"));
990 return;
991 }
992 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
993 kGpgKeyMime, payload, static_cast<uint32_t>(len));
994}
995
996static void showReceivedKeys() {
998 if (s_recvListView.getItemCount() == 0) {
999 ui::showInfo(ui::tr("mod_gpg.recv_title"), ui::tr("mod_gpg.recv_none"));
1000 return;
1001 }
1003}
1004
1005static void rebuildReceivedList() {
1006 auto& store = GpgRecvStore::instance();
1008 if (!idxBuf) {
1009 s_recvListView.init(ui::tr("mod_gpg.recv_title"), s_recvListItems, 0);
1010 return;
1011 }
1012 uint8_t n = store.listIndex(idxBuf.get(), GpgRecvStore::kMaxKeys);
1013 for (uint8_t i = 0; i < n; ++i) {
1014 gpg_recv_key_t k = {};
1015 if (!store.getKey(i, &k)) {
1016 std::snprintf(s_recvListLabels[i], sizeof(s_recvListLabels[i]), "?");
1017 } else {
1018 const char* tag = (k.sig_len > 0) ? "[x]" : "[ ]";
1019 std::snprintf(s_recvListLabels[i], sizeof(s_recvListLabels[i]),
1020 "%s %s", tag, k.user_id);
1021 }
1022 s_recvListItems[i] = { s_recvListLabels[i], 0, false,
1023 reinterpret_cast<void*>(static_cast<uintptr_t>(i)) };
1024 }
1025 s_recvListView.init(ui::tr("mod_gpg.recv_title"), s_recvListItems, n);
1027}
1028
1029static void onReceivedListSelect(uint16_t, void* userData) {
1030 s_recvSelectedIndex = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(userData));
1032 return;
1033 }
1035}
1036
1037static void showReceivedDetail() {
1038 char fp_v4[41];
1039 fp_to_hex(s_recvSelectedKey.fingerprint_v4, 20, fp_v4, sizeof(fp_v4));
1040
1041 char fp_grouped[64] = {};
1042 {
1043 size_t pos = 0;
1044 for (size_t i = 0; i < 40 && pos < sizeof(fp_grouped) - 6; i += 4) {
1045 std::memcpy(fp_grouped + pos, fp_v4 + i, 4);
1046 pos += 4;
1047 if (i + 4 < 40) fp_grouped[pos++] = ' ';
1048 }
1049 fp_grouped[pos] = '\0';
1050 }
1051
1052 const char* curveName = (s_recvSelectedKey.curve == CDC_CURVE_ED25519)
1053 ? ui::tr("mod_gpg.curve_ed25519")
1054 : ui::tr("mod_gpg.curve_p256");
1055
1056 char timeBuf[32];
1057 time_t t = static_cast<time_t>(s_recvSelectedKey.received_at);
1058 struct tm tm_v;
1059 gmtime_r(&t, &tm_v);
1060 strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M", &tm_v);
1061
1062 std::snprintf(s_recvDetailText, sizeof(s_recvDetailText),
1063 "%s\n%s\nFP: %s\nRcvd: %s\n%s",
1064 s_recvSelectedKey.user_id,
1065 curveName,
1066 fp_grouped,
1067 timeBuf,
1068 s_recvSelectedKey.sig_len > 0
1069 ? ui::tr("mod_gpg.recv_signed")
1070 : ui::tr("mod_gpg.recv_unsigned"));
1071
1072 const bool unsigned_key = (s_recvSelectedKey.sig_len == 0);
1073 s_recvActionItems[0] = { ui::tr("mod_gpg.recv_sign"), 0, false, reinterpret_cast<void*>(1) };
1074 s_recvActionItems[1] = { ui::tr("mod_gpg.recv_export"), 0, false, reinterpret_cast<void*>(2) };
1075 s_recvActionItems[2] = { ui::tr("mod_gpg.recv_send_sig"), 0, unsigned_key, reinterpret_cast<void*>(6) };
1076 s_recvActionItems[3] = { ui::tr("mod_gpg.recv_forward"), 0, false, reinterpret_cast<void*>(4) };
1077 s_recvActionItems[4] = { ui::tr("mod_gpg.recv_qr"), 0, false, reinterpret_cast<void*>(5) };
1078 s_recvActionItems[5] = { ui::tr("core.delete"), 0, false, reinterpret_cast<void*>(3) };
1079 s_recvActionView.init(ui::tr("mod_gpg.recv_details"), s_recvActionItems, 6);
1081
1082 // Show details in a static InfoView, then push the action list on top.
1083 s_infoView.init(ui::tr("mod_gpg.recv_details"), s_recvDetailText);
1086}
1087
1088static void onReceivedActionSelect(uint16_t, void* userData) {
1089 const uintptr_t action = reinterpret_cast<uintptr_t>(userData);
1090 switch (action) {
1091 case 1:
1092 ui::showConfirm(ui::tr("mod_gpg.recv_confirm_sign"),
1093 onReceivedSignConfirm, nullptr,
1095 break;
1096 case 2: {
1097 size_t out_len = 0;
1099 sizeof(s_recvExportBuf), &out_len)) {
1100 ui::showToast(ui::tr("mod_gpg.toast_sign_fail"));
1101 return;
1102 }
1103 s_infoView.init(ui::tr("mod_gpg.recv_export_title"), s_recvExportBuf);
1105 break;
1106 }
1107 case 3:
1108 ui::showConfirm(ui::tr("mod_gpg.recv_confirm_del"),
1109 onReceivedDeleteConfirm, nullptr,
1111 break;
1112 case 4: {
1113 static EXT_RAM_BSS_ATTR uint8_t payload[kGpgKeyPayloadMax];
1114 size_t len = gpgBuildRecvKeyPayload(s_recvSelectedKey, payload, sizeof(payload));
1115 if (len == 0) {
1116 ui::showToastError(ui::tr("core.failed"));
1117 return;
1118 }
1119 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
1120 kGpgKeyMime, payload, static_cast<uint32_t>(len));
1121 break;
1122 }
1123 case 5: {
1124 size_t out_len = 0;
1126 sizeof(s_recvExportBuf), &out_len)) {
1127 ui::showToastError(ui::tr("core.failed"));
1128 return;
1129 }
1130 s_qrView.init(s_recvExportBuf, s_recvSelectedKey.user_id, nullptr);
1132 break;
1133 }
1134 case 6: {
1135 // Send our cross-signature back to the key's owner; the framework
1136 // owns the peer picker, consent, encryption and progress UI.
1137 static EXT_RAM_BSS_ATTR uint8_t payload[kGpgCertPayloadMax];
1138 size_t len = gpgBuildCertPayload(s_recvSelectedKey, payload, sizeof(payload));
1139 if (len == 0) {
1140 ui::showToastError(ui::tr("core.failed"));
1141 return;
1142 }
1143 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
1144 kGpgCertMime, payload, static_cast<uint32_t>(len));
1145 break;
1146 }
1147 default: break;
1148 }
1149}
1150
1151static void onReceivedSignConfirm(void*) {
1152 uint32_t now = static_cast<uint32_t>(time(nullptr));
1153 uint8_t sig[64] = {0};
1154 if (!gpgCrossSign(s_recvSelectedKey, now, sig)) {
1155 ui::showToast(ui::tr("mod_gpg.toast_sign_fail"));
1156 return;
1157 }
1158 if (!GpgRecvStore::instance().setSignature(
1159 s_recvSelectedIndex, sig, 64, now,
1161 ui::showToast(ui::tr("mod_gpg.toast_sign_fail"));
1162 return;
1163 }
1164 // Refresh local snapshot + list label.
1167 ui::showToast(ui::tr("mod_gpg.toast_signed"));
1168}
1169
1170static void onReceivedDeleteConfirm(void*) {
1173 // Pop ActionView + InfoView back to the list.
1176 ui::showToast(ui::tr("core.deleted"));
1177}
1178
1184EXT_RAM_BSS_ATTR static char s_myCertLabels[GpgSelfCertStore::kMaxCerts][72];
1185static uint8_t s_myCertSelectedIndex = 0;
1187EXT_RAM_BSS_ATTR static char s_myCertDetailText[320];
1188
1189static void rebuildMyCertList();
1190static void onMyCertListSelect(uint16_t index, void*);
1191static void onMyCertDeleteConfirm(void*);
1192
1193static void rebuildMyCertList() {
1194 auto& store = GpgSelfCertStore::instance();
1196 if (!idxBuf) {
1197 s_myCertListView.init(ui::tr("mod_gpg.mycerts"), s_myCertListItems, 0);
1198 return;
1199 }
1200 uint8_t n = store.listIndex(idxBuf.get(), GpgSelfCertStore::kMaxCerts);
1201 for (uint8_t i = 0; i < n; ++i) {
1202 gpg_self_cert_t c = {};
1203 if (!store.getCert(i, &c)) {
1204 std::snprintf(s_myCertLabels[i], sizeof(s_myCertLabels[i]), "?");
1205 } else {
1206 std::snprintf(s_myCertLabels[i], sizeof(s_myCertLabels[i]), "%s", c.issuer_uid);
1207 }
1208 s_myCertListItems[i] = { s_myCertLabels[i], 0, false,
1209 reinterpret_cast<void*>(static_cast<uintptr_t>(i)) };
1210 }
1211 s_myCertListView.init(ui::tr("mod_gpg.mycerts"), s_myCertListItems, n);
1213}
1214
1217 if (s_myCertListView.getItemCount() == 0) {
1218 ui::showInfo(ui::tr("mod_gpg.mycerts"), ui::tr("mod_gpg.mycerts_none"));
1219 return;
1220 }
1222}
1223
1224static void onMyCertListSelect(uint16_t, void* userData) {
1225 s_myCertSelectedIndex = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(userData));
1227 return;
1228 }
1229
1230 char fp_v4[41];
1231 fp_to_hex(s_myCertSelected.issuer_fp_v4, 20, fp_v4, sizeof(fp_v4));
1232
1233 char timeBuf[32];
1234 time_t t = static_cast<time_t>(s_myCertSelected.received_at);
1235 struct tm tm_v;
1236 gmtime_r(&t, &tm_v);
1237 strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M", &tm_v);
1238
1239 std::snprintf(s_myCertDetailText, sizeof(s_myCertDetailText),
1240 "%s\nFP: %s\nRcvd: %s",
1241 s_myCertSelected.issuer_uid, fp_v4, timeBuf);
1242
1243 // Show the certification details first; deleting is a context-menu action
1244 // (key 3) on that view, so the user reads the info before any delete prompt.
1245 s_infoView.init(ui::tr("mod_gpg.mycerts_details"), s_myCertDetailText);
1246 s_infoView.setHint("[3] Delete [N] Back");
1247 s_infoView.setOnMenu([](void*) {
1248 ui::showConfirm(ui::tr("mod_gpg.mycerts_confirm_del"),
1249 onMyCertDeleteConfirm, nullptr,
1251 }, nullptr);
1253}
1254
1255static void onMyCertDeleteConfirm(void*) {
1258 ui::ViewStack::instance().pop(); // close the details view, back to the list
1259 ui::showToast(ui::tr("core.deleted"));
1260}
1261
1267 static GpgModule inst;
1268 return inst;
1269}
1270
1276 LOG_I(TAG, "Initializing GPG module");
1279
1281 if (!slotRange_.hasEcc) {
1282 core::ModuleRegistry::instance().reportModuleError(getName(), "GPG slot range missing");
1284 return false;
1285 }
1286 gpg_storage_set_slot_range(slotRange_.eccStart, slotRange_.eccEnd);
1287 if (slotRange_.hasRmem) {
1288 gpg_storage_set_rmem_range(slotRange_.rmemStart, slotRange_.rmemEnd);
1289 }
1290 core::TropicSlotMap::SlotRange rsaRange = {};
1291 if (core::TropicSlotMap::instance().getRangeByName(
1292 "mod_gpg_rsa", core::TropicSlotMap::SlotType::RMEM, &rsaRange)) {
1293 gpg_storage_set_rsa_slot_range(rsaRange.start, rsaRange.end);
1294 }
1295 if (!gpg_storage_ready()) {
1296 core::ModuleRegistry::instance().reportModuleError(getName(), "GPG slot range invalid");
1298 return false;
1299 }
1301
1302 cdc::msg::MessageTransfer::instance().registerHandler(
1303 kGpgKeyMime, "mod_gpg.received", deliverGpgKey);
1304 cdc::msg::MessageTransfer::instance().registerHandler(
1305 kGpgCertMime, "mod_gpg.cert_received", deliverGpgCert);
1306
1308 return true;
1309}
1310
1316 if (state_ != core::ServiceState::INITIALIZED &&
1317 state_ != core::ServiceState::STOPPED) {
1318 return false;
1319 }
1320
1321 core::UsbInterfaceSpec spec = {};
1323 spec.name = "OpenPGP SmartCard";
1324 spec.epInSize = 64;
1325 spec.epOutSize = 64;
1326 // Call ccid_init() rather than openpgp_init() directly: it brings up
1327 // OpenPGP and is the only external reference into ccid.cpp / ccid_driver.cpp.
1328 // Without it the linker drops the entire CCID translation unit (including
1329 // our strong usbd_app_driver_get_cb override), leaving tinyusb's weak
1330 // default in place and the smart-card interface unenumerated.
1331 if (!ccid_init()) {
1333 }
1334 if (!core::UsbManager::instance().registerInterface(core::UsbHidInterface::Ccid, getName(), spec)) {
1335 LOG_W(TAG, "Failed to register CCID interface");
1336 }
1337
1339 return true;
1340}
1341
1346 cdc::msg::MessageTransfer::instance().unregisterHandler(kGpgKeyMime);
1347 cdc::msg::MessageTransfer::instance().unregisterHandler(kGpgCertMime);
1350}
1351
1357 slotRange_ = range;
1358}
1359
1366 req.mapName = getName();
1367 req.minEccSlots = 3;
1368 req.minRmemSlots = 1;
1369 return req;
1370}
1371
1378uint8_t GpgModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
1379 if (!items || maxItems == 0) return 0;
1380 items[0] = {ui::tr("mod_gpg.title"), 60, []() -> ui::IView* {
1381 if (!s_viewsInitialized) {
1382 s_menuView.setOnSelect(onMenuSelect);
1383 s_viewsInitialized = true;
1384 }
1385 if (!GpgModule::instance().slotRange_.hasEcc || !GpgModule::instance().slotRange_.hasRmem) {
1386 ui::showToastError(ui::tr("mod_gpg.slot_error"));
1387 return nullptr;
1388 }
1389 rebuildMenu();
1390 return &s_menuView;
1391 }, nullptr, getName(), core::MenuLocation::MAIN_MENU, nullptr};
1392 return 1;
1393}
1394
1395} // namespace cdc::mod_gpg
1396
1400extern "C" void mod_gpg_register() {
1402 auto& module = cdc::mod_gpg::GpgModule::instance();
1403 module.init();
1404 });
1405}
static const char * TAG
(De)serialisation of a badge-to-badge certification return message.
Transport-agnostic (de)serialisation of a GPG public key.
void mod_gpg_register()
Registers GPG module initializer in global registry.
void gpg_storage_set_rsa_slot_range(uint16_t start, uint16_t end)
Sets the dedicated R-Memory range used for RSA private-key blobs.
void gpg_storage_set_rmem_range(uint16_t rmemStart, uint16_t rmemEnd)
bool gpg_storage_ready(void)
void gpg_storage_set_slot_range(uint16_t eccStart, uint16_t eccEnd)
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
#define KEY_FINGERPRINT_MAX_LEN
bool ccid_init(void)
Definition ccid.cpp:83
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
void reportModuleError(const char *name, const char *message)
Records and publishes an operational module error by module name.
bool registerModule(IModule *module)
Registers a module instance in the runtime registry.
static ModuleRegistry & instance()
Returns the singleton module registry instance.
void registerInitializer(ModuleInitFunc initFunc)
Registers a deferred module initializer callback.
void clearModuleErrorByName(const char *name)
Clears stored module error by module name.
static constexpr uint8_t PIN_MAX
Definition PinManager.h:53
static constexpr uint8_t PW3_MIN
Definition PinManager.h:52
static constexpr uint8_t PW1_MIN
Definition PinManager.h:51
static TropicSlotMap & instance()
Returns singleton Tropic slot-map instance.
static UsbManager & instance()
Returns singleton USB manager instance.
void unregisterInterface(UsbHidInterface type, const char *moduleName)
Unregisters a previously registered HID interface.
uint8_t getMenuItems(core::ModuleMenuItem *items, uint8_t maxItems) override
Provides main-menu entry for GPG module.
const char * getName() const override
Definition GpgModule.h:9
bool init() override
Initializes GPG module resources and slot assignments.
bool start() override
Starts GPG module and registers USB CCID interface.
static GpgModule & instance()
Returns singleton GPG module instance.
core::IModule::SlotRequest getSlotRequest() const override
Declares slot requirements for GPG module.
void stop() override
Stops GPG module and unregisters CCID interface.
void setSlotRange(const core::IModule::SlotRange &range) override
Stores slot range assigned by module registry.
bool deleteKey(uint8_t index)
Remove one key by sorted index. No-op if index is out of range.
bool getKey(uint8_t index, gpg_recv_key_t *out)
Load one key by sorted index (0..count()-1).
static GpgRecvStore & instance()
static constexpr uint8_t kMaxKeys
Hard ceiling. Past this addKey rejects further inserts.
static GpgSelfCertStore & instance()
bool deleteCert(uint8_t index)
Remove one certification by sorted index. No-op if out of range.
static constexpr uint8_t kMaxCerts
Hard ceiling. Past this addCert rejects further inserts.
static void print(const char *str)
Prints raw string to console.
Definition Console.cpp:78
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:32
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:306
void registerEnglishTable(const I18nEntry *entries, std::size_t count)
Append English entries to the lookup table.
Definition I18n.cpp:326
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
void popToAnchor(IView *anchor)
Pops views until the specified anchor view is the current view.
void push(IView *view, void *context=nullptr)
#define CDC_CURVE_ED25519
Definition fido2.h:23
#define CDC_CURVE_P256
Definition fido2.h:24
uint8_t curve
uint8_t user_id[FIDO2_USER_ID_MAX_LEN]
bool gpg_set_pending_user_id(const char *user_id)
Stages a user-id string for the next on-device key generation. The string is forwarded to OpenpgpNvsS...
Definition gpg.cpp:125
bool gpg_generate_key(uint8_t curve)
Generates SIG / DEC / AUT keys on the device and announces them to the OpenPGP card application (fing...
Definition gpg.cpp:136
bool gpg_alchemy_fingerprint(char *buf, size_t len)
Writes the alchemical-word fingerprint of the SIG public key.
Definition gpg.cpp:337
bool gpg_get_status(gpg_status_t *status)
Fills status from the OpenPGP card-application state.
Definition gpg.cpp:95
#define GPG_USER_ID_MAX
Definition gpg.h:14
#define GPG_FINGERPRINT_LEN
Definition gpg.h:15
bool gpg_reset(void)
Factory-resets all GPG key material and metadata.
Definition gpg.cpp:230
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Allocate count elements of T in PSRAM (8-bit capable region).
Definition Raii.h:51
static ui::ListItem s_myCertListItems[GpgSelfCertStore::kMaxCerts+1]
static void onReceivedDeleteConfirm(void *)
static void cmd_gpg_rsa_selftest(const char *args)
Serial self-test: runs the software RSA round-trip on the live firmware (no SE / NVS),...
static bool deliverGpgCert(const uint8_t *data, uint32_t len, const char *, const char *)
cdc_msg delivery callback: store a third-party certification on our own key, sent back by a peer that...
static char s_recvListLabels[GpgRecvStore::kMaxKeys][72]
constexpr size_t kGpgCertPayloadMax
Maximum payload size (maximum issuer uid + maximum signature packet).
static char s_myCertDetailText[320]
static ui::ListItem s_settingsItems[]
static ui::ListView s_recvActionView
static ui::ListItem makeMenuItem(const char *label, GpgMenuAction action)
static void cmd_gpg_recv_info(const char *args)
static uint8_t gpg_retries_pw3()
Returns remaining retries for OpenPGP PW3.
static bool gpg_change_pw1(const char *, const char *newPin)
Changes OpenPGP PW1 value.
static ui::QRCodeView s_qrView
static void showSettings()
Shows GPG settings menu.
static bool parse_hex(const char *args, uint8_t *out, size_t out_cap, size_t *out_len)
Decode an ASCII-hex string into out. Skips ASCII whitespace between bytes.
static constexpr uint64_t RESET_TOKEN_TIMEOUT_US
static uint64_t s_reset_token_ts_us
static void cmd_gpg_recv_cross_sign(const char *args)
static void showSendKey()
static ui::ListItem s_menuItems[8]
static void cmd_gpg_status(const char *args)
Serial command printing current GPG key status.
static void showReceivedDetail()
static bool s_viewsInitialized
static void showMyCertifications()
static void registerStrings()
Definition GpgModule.cpp:87
static char s_userPinLabel[48]
static ui::ListView s_myCertListView
My-certifications (third-party signatures on our own key) UI state.
static void cmd_gpg_mycert_delete(const char *args)
static bool gpg_verify_pw1(const char *pin)
Verifies OpenPGP PW1 using persistent pin-storage backend.
static void onReceivedSignConfirm(void *)
static ui::ListView s_settingsView
bool gpgBuildOwnSignedKeyArmored(char *out, size_t out_size, size_t *out_len)
Build an ASCII-armored OpenPGP block for the badge's own public key, appending every stored third-par...
Definition xsig.cpp:716
static constexpr const char * kGpgKeyMime
MIME type for badge-to-badge public-key transfer over cdc_msg.
Definition GpgModule.cpp:92
static ui::ListView s_menuView
static gpg_self_cert_t s_myCertSelected
static void onWizardEmail(const char *text)
Saves wizard email and opens curve selection.
static void rebuildMyCertList()
bool gpgParseCertPayload(const uint8_t *data, size_t len, gpg_self_cert_t *out, uint8_t out_target_fp[20])
Parse a certification return message into a self-cert record.
static gpg_recv_key_t s_recvSelectedKey
size_t gpgBuildRecvKeyPayload(const gpg_recv_key_t &key, uint8_t *out, size_t out_size)
Serialise a stored received key into the wire payload (for forwarding).
size_t gpgBuildOwnKeyPayload(uint8_t *out, size_t out_size)
Serialise the badge's own public key into the wire payload.
bool gpgParseKeyPayload(const uint8_t *data, size_t len, gpg_recv_key_t *out)
Parse a wire payload into a key record, computing the V5 fingerprint.
static bool gpg_change_pw3(const char *, const char *newPin)
Changes OpenPGP PW3 value.
constexpr ui::I18nEntry kStrings[]
Definition GpgModule.cpp:45
static void onMenuSelect(uint16_t, void *userData)
Handles GPG main-menu selections via the entry's userData tag.
static bool gpg_blocked_pw1()
Returns whether OpenPGP PW1 is blocked.
static void rebuildMenu()
Rebuilds GPG main menu labels and populates userData tags.
static void showExport()
Exports public key to serial output and QR view.
static void onResetConfirm(void *)
Confirm callback resetting all GPG key material.
static WizardState s_wizard
static void cmd_gpg_recv_list(const char *args)
static void onWizardCurve(uint16_t index, void *)
Finalizes wizard curve selection and triggers key generation.
static ui::ListView s_recvListView
Received-keys list UI state.
constexpr uint8_t kGpgRecvFlagVerified
Definition GpgRecvStore.h:8
constexpr size_t kGpgKeyPayloadMax
Maximum payload size (P-256 + maximum user id + DEC block).
static void cmd_gpg(const char *args)
static void showStatus()
Displays current GPG key status and metadata.
static ui::ListItem s_recvActionItems[6]
static char s_reset_token[7]
Serial command resetting GPG key material.
static void rebuildReceivedList()
static void registerCommands()
Registers serial commands exposed by GPG module.
static void cmd_gpg_recv_delete(const char *args)
static void cmd_gpg_recv_import(const char *args)
static void onReceivedListSelect(uint16_t index, void *)
static constexpr const char * CMD_MODULE
static void onMyCertDeleteConfirm(void *)
static ui::PinChangeView s_pinChangeView
static ui::ListView s_curveView
static char s_adminPinLabel[48]
static uint8_t gpg_retries_pw1()
Returns remaining retries for OpenPGP PW1.
static uint8_t s_myCertSelectedIndex
static void showReceivedKeys()
static bool deliverGpgKey(const uint8_t *data, uint32_t len, const char *, const char *)
cdc_msg delivery callback: store a public key pushed by a peer.
bool gpgCrossSign(const gpg_recv_key_t &target, uint32_t sig_creation_time, uint8_t out_sig[64])
Cross-sign a received key with the badge's own SIG ECC slot.
Definition xsig.cpp:593
static const cdc::serial::SubCommand kGpgSubs[]
bool gpgBuildPublicKeyArmored(const gpg_recv_key_t &key, char *out, size_t out_size, size_t *out_len)
Build an ASCII-armored OpenPGP public-key block (no signature).
Definition xsig.cpp:709
static char s_myCertLabels[GpgSelfCertStore::kMaxCerts][72]
static void onReceivedActionSelect(uint16_t index, void *)
static constexpr const char * kGpgCertMime
MIME type for badge-to-badge certification return messages over cdc_msg.
static void cmd_gpg_recv_export(const char *args)
static void cmd_gpg_export(const char *args)
Serial command exporting GPG public key in PEM format.
static void wizardStart()
Starts key-generation wizard flow.
static void cmd_gpg_reset(const char *args)
static void onGpgPinComplete(bool)
Pin-change completion callback returning to previous view.
static uint8_t s_recvSelectedIndex
static bool s_commandsRegistered
static bool gpg_blocked_pw3()
Returns whether OpenPGP PW3 is blocked.
static ui::ListItem s_recvListItems[GpgRecvStore::kMaxKeys+1]
static void onMyCertListSelect(uint16_t index, void *)
static char s_recvExportBuf[4096]
static void cmd_gpg_mycert_list(const char *args)
static bool parse_index(const char *args, uint8_t *out)
static void confirmReset()
Opens reset confirmation dialog.
static ui::T9InputView s_t9Input
size_t gpgBuildCertPayload(const gpg_recv_key_t &signed_key, uint8_t *out, size_t out_size)
Serialise a certification return message for a cross-signed key.
static void onSettingsSelect(uint16_t index, void *)
Handles settings-menu selection for PW1/PW3 change flow.
static char s_recvDetailText[640]
static void cmd_gpg_mycert_import(const char *args)
static bool gpg_verify_pw3(const char *pin)
Verifies OpenPGP PW3 using persistent pin-storage backend.
static void onWizardName(const char *text)
Saves wizard name and opens email step.
static void formatPinLabel(char *out, size_t outLen, const char *base, bool blocked, uint8_t retries)
Formats a PIN settings label with its remaining-retry status.
static void fp_to_hex(const uint8_t *fp, size_t len, char *out, size_t out_size)
static void cmd_gpg_generate(const char *args)
Serial command generating GPG key with selected curve and user-id.
static ui::InfoView s_infoView
bool gpgBuildReceivedKeyArmored(const gpg_recv_key_t &key, char *out, size_t out_size, size_t *out_len)
Build an ASCII-armored OpenPGP block for a received peer key.
Definition xsig.cpp:702
ICommandRegistry & getCommandRegistry()
Returns singleton command-registry interface.
void dispatchSubCommand(const char *parent, const char *args, const SubCommand *table)
Routes a sub-command line to its handler.
Definition SubCommand.h:73
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:209
void showToast(const char *message, uint16_t durationMs=1500)
Shows a plain toast message.
InfoView * showInfo(const char *title, const char *text, const char *hint=nullptr)
Shows a shared info view instance and pushes it onto the view stack.
Definition InfoView.cpp:310
void showConfirm(const char *message, ConfirmView::ConfirmCallback onConfirm, ConfirmView::CancelCallback onCancel=nullptr, ConfirmView::Icon icon=ConfirmView::Icon::QUESTION, void *userData=nullptr)
Shows a shared modal confirmation dialog instance.
void showToastSuccess(const char *message, uint16_t durationMs=1500)
Shows a success toast message.
void showToastInfo(const char *message, uint16_t durationMs=1500)
Shows an informational toast message.
void showToastError(const char *message, uint16_t durationMs=1500)
Shows an error toast message.
bool openpgp_has_any_key(void)
Reports whether any of the SIG / DEC / AUT roles has a non-zero fingerprint configured....
Definition openpgp.cpp:1081
uint8_t pin_storage_openpgp_pw1_retries(void)
bool pin_storage_openpgp_pw1_blocked(void)
bool pin_storage_openpgp_change_pw3(const char *new_pin)
uint8_t pin_storage_openpgp_pw3_retries(void)
bool pin_storage_openpgp_verify_pw1(const char *pin)
bool pin_storage_openpgp_verify_pw3(const char *pin)
bool pin_storage_openpgp_change_pw1(const char *new_pin)
bool pin_storage_openpgp_pw3_blocked(void)
bool gpg_rsa_selftest(uint16_t n_bits)
End-to-end self-test: generate a key, serialise/reload its blob, then sign+verify and encrypt+decrypt...
Definition rsa.cpp:208
Menu item registered by a module.
Definition IModule.h:29
UsbInterfaceClass cls
Definition UsbManager.h:28
One GPG public key received from another badge.
One third-party certification received over the badge's own key.
char issuer_uid[64]
Issuer user-id (display only).
uint8_t issuer_fp_v4[20]
Issuer (signer) v4 fingerprint.
Snapshot of the current OpenPGP card-application state for UI display.
Definition gpg.h:25
uint8_t fingerprint[20]
Definition gpg.h:29
Single English translation entry.
Definition I18n.h:44
const char * label
Definition ListView.h:16