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"
5#include "openpgp/xsig.h"
7#include "cdc_core/Raii.h"
10#include "cdc_ui/I18n.h"
13#include "cdc_ui/ViewStack.h"
14#include "cdc_views/ListView.h"
16#include "cdc_views/InfoView.h"
19#include "cdc_views/ToastView.h"
21#include "cdc_core/PinManager.h"
25#include "serial_cmd/Console.h"
26#include "mod_gpg/gpg.h"
27#include "cdc_log.h"
28#include "esp_timer.h"
29#include "esp_random.h"
30#include "esp_attr.h"
31#include <new>
32#include <cstring>
33#include <cstdio>
34#include <cctype>
35
36static const char* TAG = "GPG";
37
38namespace cdc::mod_gpg {
39
40constexpr ui::I18nEntry kStrings[] = {
41 {"mod_gpg.title", "GPG"},
42 {"mod_gpg.status", "Status"},
43 {"mod_gpg.generate", "Generate Keys"},
44 {"mod_gpg.export", "Export Public"},
45 {"mod_gpg.reset", "Reset"},
46 {"mod_gpg.settings", "Settings"},
47 {"mod_gpg.user_pin", "User PIN"},
48 {"mod_gpg.admin_pin", "Admin PIN"},
49 {"mod_gpg.slot_error", "Slot map error"},
50 {"mod_gpg.name", "Name"},
51 {"mod_gpg.email", "Email (optional)"},
52 {"mod_gpg.curve", "Curve"},
53 {"mod_gpg.curve_ed25519", "Ed25519"},
54 {"mod_gpg.curve_p256", "P-256"},
55 {"mod_gpg.no_key", "No key configured"},
56 {"mod_gpg.confirm_reset", "Reset all GPG keys?"},
57 {"mod_gpg.export_title", "GPG Public Key"},
58 {"mod_gpg.send", "Send Key"},
59 {"mod_gpg.received", "Received Keys"},
60 {"mod_gpg.recv_none", "No received keys"},
61 {"mod_gpg.recv_title", "Received Keys"},
62 {"mod_gpg.recv_details", "Key Details"},
63 {"mod_gpg.recv_sign", "Cross-Sign"},
64 {"mod_gpg.recv_export", "Export"},
65 {"mod_gpg.recv_delete", "Delete"},
66 {"mod_gpg.recv_confirm_sign","Sign this key?"},
67 {"mod_gpg.recv_confirm_del", "Delete this key?"},
68 {"mod_gpg.recv_signed", "Signed"},
69 {"mod_gpg.recv_unsigned", "Unsigned"},
70 {"mod_gpg.recv_export_title","Signed Key Export"},
71 {"mod_gpg.toast_signed", "Cross-signed"},
72 {"mod_gpg.toast_sign_fail", "Sign failed"},
73 {"mod_gpg.toast_deleted", "Deleted"},
74};
75
79
80static constexpr const char* CMD_MODULE = "gpg";
81static bool s_commandsRegistered = false;
82
83static void cmd_gpg_status(const char* args);
84static void cmd_gpg_generate(const char* args);
85static void cmd_gpg_export(const char* args);
86static void cmd_gpg_reset(const char* args);
87static void cmd_gpg_recv_list(const char* args);
88static void cmd_gpg_recv_info(const char* args);
89static void cmd_gpg_recv_delete(const char* args);
90static void cmd_gpg_cross_sign(const char* args);
91static void cmd_gpg_export_signed(const char* args);
92
94 {"STATUS", "", "Show keys, fingerprints, counters", cmd_gpg_status},
95 {"GENERATE", "<curve> <user_id>", "Generate SIG+DEC+AUT keys (curve 1=Ed25519, 2=P-256)", cmd_gpg_generate},
96 {"EXPORT", "", "Print primary + subkey public keys as PEM", cmd_gpg_export},
97 {"RESET", "[token]", "Two-step destructive reset of all GPG keys", cmd_gpg_reset},
98 {"RECV_LIST", "", "List received cross-sign keys", cmd_gpg_recv_list},
99 {"RECV_INFO", "<index>", "Show received key details", cmd_gpg_recv_info},
100 {"RECV_DELETE", "<index>", "Delete received key", cmd_gpg_recv_delete},
101 {"CROSS_SIGN", "<index>", "Cross-sign a received key", cmd_gpg_cross_sign},
102 {"EXPORT_SIGNED","<index>", "Export signed key as ASCII-armored OpenPGP block", cmd_gpg_export_signed},
103 {nullptr, nullptr, nullptr, nullptr},
104};
105
106static void cmd_gpg(const char* args) {
108}
109
113static void registerCommands() {
114 if (s_commandsRegistered) return;
116 auto& registry = cdc::serial::getCommandRegistry();
117 registry.registerCommand({"GPG",
118 "GPG card: STATUS/GENERATE/EXPORT/RESET",
119 cmd_gpg, CMD_MODULE, true, kGpgSubs});
120}
121
126static void cmd_gpg_status(const char* args) {
127 (void)args;
128 gpg_status_t status = {};
129 if (!gpg_get_status(&status)) {
130 cdc::serial::Console::printf("ERROR: No key configured\r\n");
131 return;
132 }
133 cdc::serial::Console::printf("User-ID: %s\r\n", status.user_id);
134 cdc::serial::Console::printf("Curve: %s\r\n",
135 status.curve == CDC_CURVE_ED25519 ? "Ed25519" : "P-256");
136 cdc::serial::Console::printf("Created: %lu\r\n", static_cast<unsigned long>(status.created_at));
137 cdc::serial::Console::printf("Sign Count: %lu\r\n", static_cast<unsigned long>(status.sign_count));
138}
139
144static void cmd_gpg_generate(const char* args) {
145 char curveBuf[8] = {};
146 char userId[GPG_USER_ID_MAX] = {};
147
148 const char* p = args;
149 while (p && *p && std::isspace(static_cast<unsigned char>(*p))) p++;
150 if (!p || !*p) {
151 cdc::serial::Console::printf("Usage: GPG GENERATE <curve> <user_id>\r\n");
152 return;
153 }
154
155 size_t i = 0;
156 while (p[i] && !std::isspace(static_cast<unsigned char>(p[i])) && i + 1 < sizeof(curveBuf)) {
157 curveBuf[i] = p[i];
158 i++;
159 }
160 curveBuf[i] = '\0';
161 p += i;
162 while (p && *p && std::isspace(static_cast<unsigned char>(*p))) p++;
163 if (!p || !*p) {
164 cdc::serial::Console::printf("Usage: GPG GENERATE <curve> <user_id>\r\n");
165 return;
166 }
167 strncpy(userId, p, sizeof(userId) - 1);
168
169 uint8_t curve = (atoi(curveBuf) == 2) ? CDC_CURVE_P256 : CDC_CURVE_ED25519;
171 bool ok = gpg_generate_key(curve);
172 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
173}
174
179static void cmd_gpg_export(const char* args) {
180 (void)args;
181 char pem_buf[2048];
182 size_t out_len = 0;
183 if (!gpg_export_pubkey_pem(pem_buf, sizeof(pem_buf), &out_len)) {
184 cdc::serial::Console::printf("ERROR\r\n");
185 return;
186 }
187 cdc::serial::Console::printf("%s\r\n", pem_buf);
188}
189
194static char s_reset_token[7] = {};
195static uint64_t s_reset_token_ts_us = 0;
196static constexpr uint64_t RESET_TOKEN_TIMEOUT_US = 30ULL * 1000ULL * 1000ULL;
197
198static void cmd_gpg_reset(const char* args) {
199 const uint64_t now = static_cast<uint64_t>(esp_timer_get_time());
200 const bool token_active = (s_reset_token[0] != '\0') &&
202
203 if (args && *args && token_active && strcmp(args, s_reset_token) == 0) {
204 memset(s_reset_token, 0, sizeof(s_reset_token));
206 bool ok = gpg_reset();
207 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
208 return;
209 }
210
211 uint8_t r[3];
212 esp_fill_random(r, sizeof(r));
213 snprintf(s_reset_token, sizeof(s_reset_token), "%02X%02X%02X", r[0], r[1], r[2]);
216 "WARNING: this wipes ALL GPG keys (SIG/DEC/AUT), the DEC backup, and PINs.\r\n"
217 "Confirm within 30s: GPG RESET %s\r\n",
219}
220
221static void fp_to_hex(const uint8_t* fp, size_t len, char* out, size_t out_size) {
222 if (out_size < len * 2 + 1) {
223 if (out_size > 0) out[0] = '\0';
224 return;
225 }
226 for (size_t i = 0; i < len; ++i) {
227 snprintf(out + i * 2, 3, "%02X", fp[i]);
228 }
229 out[len * 2] = '\0';
230}
231
232static void cmd_gpg_recv_list(const char* args) {
233 (void)args;
234 auto& store = GpgRecvStore::instance();
236 if (!buf) {
237 cdc::serial::Console::printf("ERROR: out of memory\r\n");
238 return;
239 }
240 uint8_t n = store.listIndex(buf.get(), GpgRecvStore::kMaxKeys);
241 cdc::serial::Console::printf("OK: %u received keys\r\n", static_cast<unsigned>(n));
242 for (uint8_t i = 0; i < n; ++i) {
243 gpg_recv_key_t key = {};
244 if (!store.getKey(i, &key)) continue;
245 char fp_short[9];
246 fp_to_hex(key.fingerprint_v4, 4, fp_short, sizeof(fp_short));
247 cdc::serial::Console::printf("[%u] %s\r\n", i, key.user_id);
248 cdc::serial::Console::printf(" FP: %s...\r\n", fp_short);
249 cdc::serial::Console::printf(" Signed: %s\r\n",
250 key.sig_len > 0 ? "Yes" : "No");
251 }
252}
253
254static bool parse_index(const char* args, uint8_t* out) {
255 if (!args || !*args) return false;
256 char* end = nullptr;
257 long v = strtol(args, &end, 10);
258 if (end == args || v < 0 || v > 255) return false;
259 *out = static_cast<uint8_t>(v);
260 return true;
261}
262
263static void cmd_gpg_recv_info(const char* args) {
264 uint8_t idx = 0;
265 if (!parse_index(args, &idx)) {
266 cdc::serial::Console::printf("Usage: GPG RECV_INFO <index>\r\n");
267 return;
268 }
269 gpg_recv_key_t key = {};
270 if (!GpgRecvStore::instance().getKey(idx, &key)) {
271 cdc::serial::Console::printf("ERROR: index not found\r\n");
272 return;
273 }
274 char fp_v4[41];
275 char fp_v5[65];
276 fp_to_hex(key.fingerprint_v4, 20, fp_v4, sizeof(fp_v4));
277 fp_to_hex(key.fingerprint_v5, 32, fp_v5, sizeof(fp_v5));
278
279 char ts_buf[32];
280 time_t t = static_cast<time_t>(key.received_at);
281 struct tm tm_v;
282 gmtime_r(&t, &tm_v);
283 strftime(ts_buf, sizeof(ts_buf), "%Y-%m-%d %H:%M:%S UTC", &tm_v);
284
285 cdc::serial::Console::printf("OK: Key details\r\n");
286 cdc::serial::Console::printf("User-ID: %s\r\n", key.user_id);
287 cdc::serial::Console::printf("Curve: %s\r\n",
288 key.curve == CDC_CURVE_ED25519 ? "Ed25519" : "P-256");
289 cdc::serial::Console::printf("Fingerprint V4: %s\r\n", fp_v4);
290 cdc::serial::Console::printf("Fingerprint V5: %s\r\n", fp_v5);
291 cdc::serial::Console::printf("Received: %s\r\n", ts_buf);
292 cdc::serial::Console::printf("Signed: %s\r\n", key.sig_len > 0 ? "Yes" : "No");
293 cdc::serial::Console::printf("Verified: %s\r\n",
294 (key.flags & kGpgRecvFlagVerified) ? "Yes" : "No");
295 if (key.sig_len > 0) {
296 char sig_hex[2 * 64 + 1];
297 fp_to_hex(key.my_signature, key.sig_len, sig_hex, sizeof(sig_hex));
298 cdc::serial::Console::printf("Signature: %s\r\n", sig_hex);
299 }
300}
301
302static void cmd_gpg_recv_delete(const char* args) {
303 uint8_t idx = 0;
304 if (!parse_index(args, &idx)) {
305 cdc::serial::Console::printf("Usage: GPG RECV_DELETE <index>\r\n");
306 return;
307 }
309 ? "OK\r\n"
310 : "ERROR: delete failed\r\n");
311}
312
313static void cmd_gpg_cross_sign(const char* args) {
314 uint8_t idx = 0;
315 if (!parse_index(args, &idx)) {
316 cdc::serial::Console::printf("Usage: GPG CROSS_SIGN <index>\r\n");
317 return;
318 }
319 gpg_recv_key_t key = {};
320 if (!GpgRecvStore::instance().getKey(idx, &key)) {
321 cdc::serial::Console::printf("ERROR: index not found\r\n");
322 return;
323 }
324 uint32_t now = static_cast<uint32_t>(time(nullptr));
325 uint8_t sig[64] = {0};
326 if (!gpgCrossSign(key, now, sig)) {
327 cdc::serial::Console::printf("ERROR: signing failed\r\n");
328 return;
329 }
330 if (!GpgRecvStore::instance().setSignature(idx, sig, 64,
332 cdc::serial::Console::printf("ERROR: store update failed\r\n");
333 return;
334 }
336}
337
338static void cmd_gpg_export_signed(const char* args) {
339 uint8_t idx = 0;
340 if (!parse_index(args, &idx)) {
341 cdc::serial::Console::printf("Usage: GPG EXPORT_SIGNED <index>\r\n");
342 return;
343 }
344 gpg_recv_key_t key = {};
345 if (!GpgRecvStore::instance().getKey(idx, &key)) {
346 cdc::serial::Console::printf("ERROR: index not found\r\n");
347 return;
348 }
349 if (key.sig_len == 0) {
350 cdc::serial::Console::printf("ERROR: key not yet signed (use GPG CROSS_SIGN first)\r\n");
351 return;
352 }
353
354 auto buf = ::cdc::core::psramAlloc<char>(4096);
355 if (!buf) {
356 cdc::serial::Console::printf("ERROR: out of memory\r\n");
357 return;
358 }
359 size_t out_len = 0;
360 if (!gpgBuildSignedKeyArmored(key, buf.get(), 4096, &out_len)) {
361 cdc::serial::Console::printf("ERROR: export failed\r\n");
362 return;
363 }
364 cdc::serial::Console::printf("%s", buf.get());
365}
366
374static bool s_viewsInitialized = false;
377 { nullptr, 0, false, nullptr },
378 { nullptr, 0, false, nullptr },
379};
380
382 char name[64];
383 char email[64];
384 uint8_t curve;
385};
386
387EXT_RAM_BSS_ATTR static WizardState s_wizard = {};
388
398
399static void showStatus();
400static void wizardStart();
401static void showExport();
402static void confirmReset();
403static void showSettings();
404static void onSettingsSelect(uint16_t index, void*);
405static void showSendKey();
406static void showReceivedKeys();
407
411static void onMenuSelect(uint16_t, void* userData) {
412 switch (static_cast<GpgMenuAction>(reinterpret_cast<uintptr_t>(userData))) {
413 case GPG_MENU_STATUS: showStatus(); break;
414 case GPG_MENU_GENERATE: wizardStart(); break;
415 case GPG_MENU_EXPORT: showExport(); break;
416 case GPG_MENU_SEND: showSendKey(); break;
417 case GPG_MENU_RECEIVED: showReceivedKeys(); break;
418 case GPG_MENU_SETTINGS: showSettings(); break;
419 case GPG_MENU_RESET: confirmReset(); break;
420 default: break;
421 }
422}
423
424static inline ui::ListItem makeMenuItem(const char* label, GpgMenuAction action) {
425 return { label, 0, false, reinterpret_cast<void*>(static_cast<uintptr_t>(action)) };
426}
427
431static void rebuildMenu() {
432 const bool hasKeys = openpgp_has_any_key();
433 uint8_t count = 0;
434 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.status"), GPG_MENU_STATUS);
435 if (!hasKeys) {
436 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.generate"), GPG_MENU_GENERATE);
437 } else {
438 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.export"), GPG_MENU_EXPORT);
439 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.send"), GPG_MENU_SEND);
440 }
441 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.received"), GPG_MENU_RECEIVED);
442 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.settings"), GPG_MENU_SETTINGS);
443 s_menuItems[count++] = makeMenuItem(ui::tr("mod_gpg.reset"), GPG_MENU_RESET);
444 s_menuView.init(ui::tr("mod_gpg.title"), s_menuItems, count);
445}
446
452static bool gpg_verify_pw1(const char* pin) {
454}
455
461static bool gpg_verify_pw3(const char* pin) {
463}
464
471static bool gpg_change_pw1(const char*, const char* newPin) {
472 return pin_storage_openpgp_change_pw1(newPin);
473}
474
481static bool gpg_change_pw3(const char*, const char* newPin) {
482 return pin_storage_openpgp_change_pw3(newPin);
483}
484
489static uint8_t gpg_retries_pw1() {
491}
492
497static uint8_t gpg_retries_pw3() {
499}
500
505static bool gpg_blocked_pw1() {
507}
508
513static bool gpg_blocked_pw3() {
515}
516
521static void onGpgPinComplete(bool) {
523}
524
530static void onSettingsSelect(uint16_t index, void*) {
531 s_pinChangeView.setOnComplete(onGpgPinComplete);
532 s_pinChangeView.setTitle(index == 0 ? ui::tr("mod_gpg.user_pin") : ui::tr("mod_gpg.admin_pin"));
533 if (index == 0) {
534 s_pinChangeView.setVerifyCallback(gpg_verify_pw1);
535 s_pinChangeView.setChangeCallback(gpg_change_pw1);
536 s_pinChangeView.setRetriesCallback(gpg_retries_pw1);
537 s_pinChangeView.setBlockedCallback(gpg_blocked_pw1);
539 } else {
540 s_pinChangeView.setVerifyCallback(gpg_verify_pw3);
541 s_pinChangeView.setChangeCallback(gpg_change_pw3);
542 s_pinChangeView.setRetriesCallback(gpg_retries_pw3);
543 s_pinChangeView.setBlockedCallback(gpg_blocked_pw3);
545 }
547}
548
552static void showSettings() {
553 s_settingsItems[0].label = ui::tr("mod_gpg.user_pin");
554 s_settingsItems[1].label = ui::tr("mod_gpg.admin_pin");
555 s_settingsView.init(ui::tr("mod_gpg.settings"), s_settingsItems, 2);
556 s_settingsView.setOnSelect(onSettingsSelect);
558}
559
563static void showStatus() {
564 gpg_status_t status = {};
565 if (!gpg_get_status(&status)) {
566 s_infoView.init(ui::tr("mod_gpg.status"), ui::tr("mod_gpg.no_key"));
568 return;
569 }
570
571 char fp_hex[GPG_FINGERPRINT_LEN * 2 + 1] = {};
572 for (size_t i = 0; i < GPG_FINGERPRINT_LEN; i++) {
573 snprintf(fp_hex + i * 2, 3, "%02X", status.fingerprint[i]);
574 }
575 const char* curveName = status.curve == CDC_CURVE_ED25519 ? ui::tr("mod_gpg.curve_ed25519")
576 : ui::tr("mod_gpg.curve_p256");
577 static char detail[512];
578 snprintf(detail, sizeof(detail),
579 "User-ID: %s\nCurve: %s\nFP: %s\nCreated: %lu\nSign Count: %lu",
580 status.user_id, curveName, fp_hex,
581 static_cast<unsigned long>(status.created_at),
582 static_cast<unsigned long>(status.sign_count));
583 s_infoView.init(ui::tr("mod_gpg.status"), detail);
585}
586
587static void onWizardName(const char* text);
588static void onWizardEmail(const char* text);
589static void onWizardCurve(uint16_t index, void*);
590
594static void wizardStart() {
595 memset(&s_wizard, 0, sizeof(s_wizard));
596 s_t9Input.init(ui::tr("mod_gpg.name"), nullptr, 63);
597 s_t9Input.setOnSave(onWizardName);
599}
600
605static void onWizardName(const char* text) {
606 strncpy(s_wizard.name, text ? text : "", sizeof(s_wizard.name) - 1);
607 s_t9Input.init(ui::tr("mod_gpg.email"), nullptr, 63);
608 s_t9Input.setOnSave(onWizardEmail);
610}
611
616static void onWizardEmail(const char* text) {
617 strncpy(s_wizard.email, text ? text : "", sizeof(s_wizard.email) - 1);
618 static ui::ListItem curveItems[] = {
619 { nullptr, 0, false, nullptr },
620 { nullptr, 0, false, nullptr },
621 };
622 curveItems[0].label = ui::tr("mod_gpg.curve_ed25519");
623 curveItems[1].label = ui::tr("mod_gpg.curve_p256");
624 s_curveView.init(ui::tr("mod_gpg.curve"), curveItems, 2);
625 s_curveView.setOnSelect(onWizardCurve);
627}
628
634static void onWizardCurve(uint16_t index, void*) {
635 s_wizard.curve = (index == 0) ? CDC_CURVE_ED25519 : CDC_CURVE_P256;
636 char user_id[GPG_USER_ID_MAX] = {};
637 size_t name_len = strnlen(s_wizard.name, sizeof(s_wizard.name) - 1);
638 size_t email_len = strnlen(s_wizard.email, sizeof(s_wizard.email) - 1);
639 if (email_len == 0) {
640 snprintf(user_id, sizeof(user_id), "%.*s", static_cast<int>(sizeof(user_id) - 1), s_wizard.name);
641 } else {
642 size_t max_len = sizeof(user_id) - 1;
643 size_t name_fit = name_len > max_len ? max_len : name_len;
644 size_t email_fit = 0;
645 if (name_fit < max_len) {
646 size_t remaining = max_len - name_fit;
647 if (remaining > 3) {
648 email_fit = remaining - 3;
649 }
650 }
651 if (email_fit == 0) {
652 memcpy(user_id, s_wizard.name, name_fit);
653 user_id[name_fit] = '\0';
654 } else {
655 size_t pos = 0;
656 memcpy(user_id + pos, s_wizard.name, name_fit);
657 pos += name_fit;
658 user_id[pos++] = ' ';
659 user_id[pos++] = '<';
660 memcpy(user_id + pos, s_wizard.email, email_fit);
661 pos += email_fit;
662 user_id[pos++] = '>';
663 user_id[pos] = '\0';
664 }
665 }
667 if (gpg_generate_key(s_wizard.curve)) {
668 ui::showToastSuccess(ui::tr("core.ok"));
669 } else {
670 ui::showToastError(ui::tr("core.failed"));
671 }
672 while (ui::ViewStack::instance().depth() > 1) {
674 }
675}
676
680static void showExport() {
681 EXT_RAM_BSS_ATTR static char pem_buf[2048];
682 static char title_buf[GPG_USER_ID_MAX + 8];
683 static char detail_buf[160];
684 size_t out_len = 0;
685 if (!gpg_export_pubkey_pem(pem_buf, sizeof(pem_buf), &out_len)) {
686 ui::showToastError(ui::tr("core.failed"));
687 return;
688 }
689 cdc::serial::Console::printf("%s\r\n", pem_buf);
690
691 gpg_status_t status = {};
692 title_buf[0] = '\0';
693 detail_buf[0] = '\0';
694 if (gpg_get_status(&status) && status.initialized) {
695 strlcpy(title_buf,
696 status.user_id[0] ? status.user_id : "(card)",
697 sizeof(title_buf));
698
699 char alchemy[KEY_FINGERPRINT_MAX_LEN] = {};
700 gpg_alchemy_fingerprint(alchemy, sizeof(alchemy));
701 const char* curveName = (status.curve == CDC_CURVE_ED25519) ? "Ed25519" : "P-256";
702 snprintf(detail_buf, sizeof(detail_buf), "%s\n%s", curveName, alchemy);
703 }
704
705 s_qrView.init(pem_buf,
706 title_buf[0] ? title_buf : nullptr,
707 detail_buf[0] ? detail_buf : nullptr);
709}
710
715static void onResetConfirm(void*) {
716 if (gpg_reset()) {
717 ui::showToastSuccess(ui::tr("core.ok"));
718 } else {
719 ui::showToastError(ui::tr("core.failed"));
720 }
721}
722
726static void confirmReset() {
727 ui::showConfirm(ui::tr("mod_gpg.confirm_reset"), onResetConfirm, nullptr,
729}
730
738EXT_RAM_BSS_ATTR static char s_recvListLabels[GpgRecvStore::kMaxKeys][72];
739static uint8_t s_recvSelectedIndex = 0;
741EXT_RAM_BSS_ATTR static char s_recvDetailText[640];
742EXT_RAM_BSS_ATTR static char s_recvExportBuf[4096];
743
744static void rebuildReceivedList();
745static void onReceivedListSelect(uint16_t index, void*);
746static void showReceivedDetail();
747static void onReceivedActionSelect(uint16_t index, void*);
748static void onReceivedSignConfirm(void*);
749static void onReceivedDeleteConfirm(void*);
750
751static void showSendKey() {
752 // BLE handler is wired in a later step. For now show a placeholder toast so
753 // the menu entry is discoverable but not misleading.
754 ui::showToast("Send Key (BLE) - WIP");
755}
756
757static void showReceivedKeys() {
759 if (s_recvListView.getItemCount() == 0) {
760 ui::showInfo(ui::tr("mod_gpg.recv_title"), ui::tr("mod_gpg.recv_none"));
761 return;
762 }
764}
765
766static void rebuildReceivedList() {
767 auto& store = GpgRecvStore::instance();
769 if (!idxBuf) {
770 s_recvListView.init(ui::tr("mod_gpg.recv_title"), s_recvListItems, 0);
771 return;
772 }
773 uint8_t n = store.listIndex(idxBuf.get(), GpgRecvStore::kMaxKeys);
774 for (uint8_t i = 0; i < n; ++i) {
775 gpg_recv_key_t k = {};
776 if (!store.getKey(i, &k)) {
777 std::snprintf(s_recvListLabels[i], sizeof(s_recvListLabels[i]), "?");
778 } else {
779 const char* tag = (k.sig_len > 0) ? "[x]" : "[ ]";
780 std::snprintf(s_recvListLabels[i], sizeof(s_recvListLabels[i]),
781 "%s %s", tag, k.user_id);
782 }
783 s_recvListItems[i] = { s_recvListLabels[i], 0, false,
784 reinterpret_cast<void*>(static_cast<uintptr_t>(i)) };
785 }
786 s_recvListView.init(ui::tr("mod_gpg.recv_title"), s_recvListItems, n);
788}
789
790static void onReceivedListSelect(uint16_t, void* userData) {
791 s_recvSelectedIndex = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(userData));
793 return;
794 }
796}
797
798static void showReceivedDetail() {
799 char fp_v4[41];
800 fp_to_hex(s_recvSelectedKey.fingerprint_v4, 20, fp_v4, sizeof(fp_v4));
801
802 char fp_grouped[64] = {};
803 {
804 size_t pos = 0;
805 for (size_t i = 0; i < 40 && pos < sizeof(fp_grouped) - 6; i += 4) {
806 std::memcpy(fp_grouped + pos, fp_v4 + i, 4);
807 pos += 4;
808 if (i + 4 < 40) fp_grouped[pos++] = ' ';
809 }
810 fp_grouped[pos] = '\0';
811 }
812
813 const char* curveName = (s_recvSelectedKey.curve == CDC_CURVE_ED25519)
814 ? ui::tr("mod_gpg.curve_ed25519")
815 : ui::tr("mod_gpg.curve_p256");
816
817 char timeBuf[32];
818 time_t t = static_cast<time_t>(s_recvSelectedKey.received_at);
819 struct tm tm_v;
820 gmtime_r(&t, &tm_v);
821 strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M", &tm_v);
822
823 std::snprintf(s_recvDetailText, sizeof(s_recvDetailText),
824 "%s\n%s\nFP: %s\nRcvd: %s\n%s",
825 s_recvSelectedKey.user_id,
826 curveName,
827 fp_grouped,
828 timeBuf,
829 s_recvSelectedKey.sig_len > 0
830 ? ui::tr("mod_gpg.recv_signed")
831 : ui::tr("mod_gpg.recv_unsigned"));
832
833 s_recvActionItems[0] = { ui::tr("mod_gpg.recv_sign"), 0, false, reinterpret_cast<void*>(1) };
834 s_recvActionItems[1] = { ui::tr("mod_gpg.recv_export"), 0,
835 s_recvSelectedKey.sig_len == 0, reinterpret_cast<void*>(2) };
836 s_recvActionItems[2] = { ui::tr("mod_gpg.recv_delete"), 0, false, reinterpret_cast<void*>(3) };
837 s_recvActionView.init(ui::tr("mod_gpg.recv_details"), s_recvActionItems, 3);
839
840 // Show details in a static InfoView, then push the action list on top.
841 s_infoView.init(ui::tr("mod_gpg.recv_details"), s_recvDetailText);
844}
845
846static void onReceivedActionSelect(uint16_t, void* userData) {
847 const uintptr_t action = reinterpret_cast<uintptr_t>(userData);
848 switch (action) {
849 case 1:
850 ui::showConfirm(ui::tr("mod_gpg.recv_confirm_sign"),
851 onReceivedSignConfirm, nullptr,
853 break;
854 case 2: {
855 size_t out_len = 0;
857 sizeof(s_recvExportBuf), &out_len)) {
858 ui::showToast(ui::tr("mod_gpg.toast_sign_fail"));
859 return;
860 }
861 s_infoView.init(ui::tr("mod_gpg.recv_export_title"), s_recvExportBuf);
863 break;
864 }
865 case 3:
866 ui::showConfirm(ui::tr("mod_gpg.recv_confirm_del"),
869 break;
870 default: break;
871 }
872}
873
874static void onReceivedSignConfirm(void*) {
875 uint32_t now = static_cast<uint32_t>(time(nullptr));
876 uint8_t sig[64] = {0};
877 if (!gpgCrossSign(s_recvSelectedKey, now, sig)) {
878 ui::showToast(ui::tr("mod_gpg.toast_sign_fail"));
879 return;
880 }
881 if (!GpgRecvStore::instance().setSignature(
882 s_recvSelectedIndex, sig, 64,
884 ui::showToast(ui::tr("mod_gpg.toast_sign_fail"));
885 return;
886 }
887 // Refresh local snapshot + list label.
890 ui::showToast(ui::tr("mod_gpg.toast_signed"));
891}
892
893static void onReceivedDeleteConfirm(void*) {
896 // Pop ActionView + InfoView back to the list.
899 ui::showToast(ui::tr("mod_gpg.toast_deleted"));
900}
901
907 static GpgModule inst;
908 return inst;
909}
910
916 LOG_I(TAG, "Initializing GPG module");
919
921 if (!slotRange_.hasEcc) {
922 core::ModuleRegistry::instance().reportModuleError(getName(), "GPG slot range missing");
924 return false;
925 }
926 gpg_storage_set_slot_range(slotRange_.eccStart, slotRange_.eccEnd);
927 if (slotRange_.hasRmem) {
928 gpg_storage_set_rmem_range(slotRange_.rmemStart, slotRange_.rmemEnd);
929 }
930 if (!gpg_storage_ready()) {
931 core::ModuleRegistry::instance().reportModuleError(getName(), "GPG slot range invalid");
933 return false;
934 }
937 return true;
938}
939
945 if (state_ != core::ServiceState::INITIALIZED &&
946 state_ != core::ServiceState::STOPPED) {
947 return false;
948 }
949
950 core::UsbInterfaceSpec spec = {};
952 spec.name = "OpenPGP SmartCard";
953 spec.epInSize = 64;
954 spec.epOutSize = 64;
955 // Call ccid_init() rather than openpgp_init() directly: it brings up
956 // OpenPGP and is the only external reference into ccid.cpp / ccid_driver.cpp.
957 // Without it the linker drops the entire CCID translation unit (including
958 // our strong usbd_app_driver_get_cb override), leaving tinyusb's weak
959 // default in place and the smart-card interface unenumerated.
960 if (!ccid_init()) {
962 }
963 if (!core::UsbManager::instance().registerInterface(core::UsbHidInterface::Ccid, getName(), spec)) {
964 LOG_W(TAG, "Failed to register CCID interface");
965 }
966
967 // Idempotent; logs and returns false if BLE controller isn't ready yet,
968 // in which case nothing else here changes (CCID is independent).
971 // Future: invalidate the cached received-keys ListView. The UI rebuilds
972 // on demand each time it is opened so no immediate action is required.
973 });
974
976 return true;
977}
978
986
992 slotRange_ = range;
993}
994
1001 req.mapName = getName();
1002 req.minEccSlots = 3;
1003 req.minRmemSlots = 1;
1004 return req;
1005}
1006
1013uint8_t GpgModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
1014 if (!items || maxItems == 0) return 0;
1015 items[0] = {ui::tr("mod_gpg.title"), 60, []() -> ui::IView* {
1016 if (!s_viewsInitialized) {
1017 s_menuView.setOnSelect(onMenuSelect);
1018 s_viewsInitialized = true;
1019 }
1020 if (!GpgModule::instance().slotRange_.hasEcc || !GpgModule::instance().slotRange_.hasRmem) {
1021 ui::showToastError(ui::tr("mod_gpg.slot_error"));
1022 return nullptr;
1023 }
1024 rebuildMenu();
1025 return &s_menuView;
1026 }, nullptr, getName(), core::MenuLocation::MAIN_MENU, nullptr};
1027 return 1;
1028}
1029
1030} // namespace cdc::mod_gpg
1031
1035extern "C" void mod_gpg_register() {
1037 auto& module = cdc::mod_gpg::GpgModule::instance();
1038 module.init();
1039 });
1040}
static const char * TAG
void mod_gpg_register()
Registers GPG module initializer in global registry.
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 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 void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:30
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
void registerEnglishTable(const I18nEntry *entries, std::size_t count)
Append English entries to the lookup table.
Definition I18n.cpp:307
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
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:121
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:132
bool gpg_alchemy_fingerprint(char *buf, size_t len)
Writes the alchemical-word fingerprint of the SIG public key.
Definition gpg.cpp:319
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
bool gpg_export_pubkey_pem(char *buf, size_t size, size_t *out_len)
Renders the current SIG public key as a SubjectPublicKeyInfo PEM. The key is read straight from the s...
Definition gpg.cpp:233
#define GPG_FINGERPRINT_LEN
Definition gpg.h:15
bool gpg_reset(void)
Factory-resets all GPG key material and metadata.
Definition gpg.cpp:224
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Allocate count elements of T in PSRAM (8-bit capable region).
Definition Raii.h:51
static void onReceivedDeleteConfirm(void *)
static char s_recvListLabels[GpgRecvStore::kMaxKeys][72]
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 constexpr uint64_t RESET_TOKEN_TIMEOUT_US
static uint64_t s_reset_token_ts_us
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 registerStrings()
Definition GpgModule.cpp:76
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
static ui::ListView s_menuView
static void onWizardEmail(const char *text)
Saves wizard email and opens curve selection.
static void cmd_gpg_cross_sign(const char *args)
static gpg_recv_key_t s_recvSelectedKey
static bool gpg_change_pw3(const char *, const char *newPin)
Changes OpenPGP PW3 value.
constexpr ui::I18nEntry kStrings[]
Definition GpgModule.cpp:40
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
static void cmd_gpg(const char *args)
static void showStatus()
Displays current GPG key status and metadata.
bool ble_gpg_xsig_init()
Initialise the GPG cross-sign BLE endpoint.
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)
void ble_gpg_xsig_set_received_callback(XsigReceivedCallback cb)
Install / remove the "key received" notification.
static void onReceivedListSelect(uint16_t index, void *)
static constexpr const char * CMD_MODULE
Definition GpgModule.cpp:80
static ui::PinChangeView s_pinChangeView
static ui::ListView s_curveView
static uint8_t gpg_retries_pw1()
Returns remaining retries for OpenPGP PW1.
static void showReceivedKeys()
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:193
static const cdc::serial::SubCommand kGpgSubs[]
Definition GpgModule.cpp:93
static void onReceivedActionSelect(uint16_t index, void *)
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
Definition GpgModule.cpp:81
static bool gpg_blocked_pw3()
Returns whether OpenPGP PW3 is blocked.
static ui::ListItem s_recvListItems[GpgRecvStore::kMaxKeys+1]
static char s_recvExportBuf[4096]
static bool parse_index(const char *args, uint8_t *out)
static void confirmReset()
Opens reset confirmation dialog.
static ui::T9InputView s_t9Input
static void cmd_gpg_export_signed(const char *args)
static void onSettingsSelect(uint16_t index, void *)
Handles settings-menu selection for PW1/PW3 change flow.
static char s_recvDetailText[640]
static bool gpg_verify_pw3(const char *pin)
Verifies OpenPGP PW3 using persistent pin-storage backend.
bool gpgBuildSignedKeyArmored(const gpg_recv_key_t &key, char *out, size_t out_size, size_t *out_len)
Build an ASCII-armored OpenPGP block carrying the cross-signed key.
Definition xsig.cpp:283
static void onWizardName(const char *text)
Saves wizard name and opens email step.
static ui::ListItem s_recvActionItems[3]
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
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:208
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 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:907
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)
Menu item registered by a module.
Definition IModule.h:29
UsbInterfaceClass cls
Definition UsbManager.h:28
One GPG public key received from another badge.
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