CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
TwoFaModule.cpp
Go to the documentation of this file.
2#include "mod_2fa/OathStore.h"
10#include "cdc_ui/I18n.h"
11#include "cdc_ui/ViewStack.h"
12#include "cdc_views/ListView.h"
14#include "cdc_views/ToastView.h"
16#include "cdc_hal/IDisplay.h"
19#include "serial_cmd/Console.h"
20#include "cdc_log.h"
21#include "esp_attr.h"
22#include "cJSON.h"
23#include <goodisplay/gdey029T94.h>
24#include <cctype>
25#include <cstdlib>
26#include <cstring>
27#include <new>
28
29static const char* TAG = "2FA";
30
31namespace cdc::mod_2fa {
32
33constexpr ui::I18nEntry kStrings[] = {
34 {"mod_2fa.title", "2FA"},
35 {"mod_2fa.add_account", "Add Account"},
36 {"mod_2fa.account_name", "Account Name"},
37 {"mod_2fa.secret", "Secret (Base32)"},
38 {"mod_2fa.issuer", "Issuer (optional)"},
39 {"mod_2fa.digits", "Digits"},
40 {"mod_2fa.algorithm", "Algorithm"},
41 {"mod_2fa.period", "Period"},
42 {"mod_2fa.code", "Code"},
43 {"mod_2fa.time_invalid", "Time not set"},
44 {"mod_2fa.invalid_input", "Invalid input"},
45 {"mod_2fa.hint_edit", "[3] Edit [N] Back"},
46 {"mod_2fa.hint_type", "[Y] Type [3] Edit [N] Back"},
47 {"mod_2fa.hint_hotp", "[5] Next [Y] Type [3] Edit [N] Back"},
48 {"mod_2fa.no_keyboard", "No keyboard connected"},
49 {"mod_2fa.type", "Type"},
50 {"mod_2fa.counter", "Counter"},
51 {"mod_2fa.touch", "Touch confirm"},
52 {"mod_2fa.touch_on", "Required"},
53 {"mod_2fa.touch_off", "Not required"},
54 {"mod_2fa.cr_confirm", "Allow challenge-response?"},
55 {"mod_2fa.cr_entry", "Challenge-Response"},
56 {"mod_2fa.usb_cr", "USB slot 2"},
57 {"mod_2fa.usb_cr_on", "Designate"},
58 {"mod_2fa.usb_cr_off", "No"},
59};
60
64
66
67static constexpr const char* CMD_MODULE = "totp";
68static constexpr size_t SECRET_B32_LEN = 128;
69static bool s_commandsRegistered = false;
70
73
79static uint8_t parseAlgo(const char* token) {
80 if (!token || !*token) return static_cast<uint8_t>(OathAlgorithm::SHA1);
81 char buf[8] = {};
82 size_t i = 0;
83 for (; token[i] && i + 1 < sizeof(buf); i++) {
84 buf[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(token[i])));
85 }
86 buf[i] = '\0';
87 if (strcmp(buf, "sha1") == 0) return static_cast<uint8_t>(OathAlgorithm::SHA1);
88 if (strcmp(buf, "sha256") == 0) return static_cast<uint8_t>(OathAlgorithm::SHA256);
89 if (strcmp(buf, "sha512") == 0) return static_cast<uint8_t>(OathAlgorithm::SHA512);
90 // Numeric fallback with bounds check: only accept supported algorithm IDs.
91 int value = atoi(buf);
92 if (value < 0 || value > static_cast<int>(OathAlgorithm::SHA512)) {
93 return static_cast<uint8_t>(OathAlgorithm::SHA1);
94 }
95 return static_cast<uint8_t>(value);
96}
97
103static uint8_t parseType(const char* token) {
104 if (!token || !*token) return static_cast<uint8_t>(OathType::TOTP);
105 char buf[8] = {};
106 size_t i = 0;
107 for (; token[i] && i + 1 < sizeof(buf); i++) {
108 buf[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(token[i])));
109 }
110 buf[i] = '\0';
111 if (strcmp(buf, "totp") == 0) return static_cast<uint8_t>(OathType::TOTP);
112 if (strcmp(buf, "hotp") == 0) return static_cast<uint8_t>(OathType::HOTP);
113 if (strcmp(buf, "cr") == 0) return static_cast<uint8_t>(OathType::CR);
114 int value = atoi(buf);
115 if (value == static_cast<int>(OathType::HOTP)) return static_cast<uint8_t>(OathType::HOTP);
116 if (value == static_cast<int>(OathType::CR)) return static_cast<uint8_t>(OathType::CR);
117 return static_cast<uint8_t>(OathType::TOTP);
118}
119
126static bool findSlotByIndex(uint16_t index, uint16_t* slotOut) {
127 if (!slotOut) return false;
128 auto& store = OathStore::instance();
129 if (!store.hasSlotRange()) return false;
130 struct Ctx {
131 uint16_t target;
132 uint16_t current;
133 uint16_t slot;
134 bool found;
135 } ctx = { index, 0, 0, false };
136
137 auto cb = [](uint16_t slot, const cdc::core::TropicStorage::CacheEntry&, void* user) {
138 auto* c = static_cast<Ctx*>(user);
139 if (c->found) return;
140 uint16_t logical = 0;
141 if (!OathStore::instance().toLogicalSlot(slot, &logical)) return;
142 if (c->current == c->target) {
143 c->slot = logical;
144 c->found = true;
145 return;
146 }
147 c->current++;
148 };
149
151 store.moduleId(),
152 store.rmemStart(),
153 store.rmemEnd(),
154 cb, &ctx);
155
156 if (!ctx.found) return false;
157 *slotOut = ctx.slot;
158 return true;
159}
160
165static void cmd_totp_list(const char* args) {
166 (void)args;
167 if (!OathStore::instance().hasSlotRange()) {
168 cdc::serial::Console::printf("ERROR: slot map not configured\r\n");
169 return;
170 }
171 struct ListCtx {
172 uint16_t idx;
173 } ctx = {0};
174 auto cb = [](uint16_t slot, const cdc::core::TropicStorage::CacheEntry& entry, void* user) {
175 auto* c = static_cast<ListCtx*>(user);
176 uint16_t logical = 0;
177 if (!OathStore::instance().toLogicalSlot(slot, &logical)) return;
178 OathEntry account = {};
179 const char* typeStr = "?";
180 if (OathStore::instance().readAccount(logical, &account)) {
181 switch (static_cast<OathType>(account.type)) {
182 case OathType::HOTP: typeStr = "HOTP"; break;
183 case OathType::CR: typeStr = "CR"; break;
184 default: typeStr = "TOTP"; break;
185 }
186 }
187 cdc::serial::Console::printf("%u: %s [%s] (slot %u)\r\n", c->idx, entry.name, typeStr, logical);
188 c->idx++;
189 };
190
193 OathStore::instance().rmemStart(),
194 OathStore::instance().rmemEnd(),
195 cb, &ctx);
196
197 if (ctx.idx == 0) {
198 cdc::serial::Console::printf("(no entries)\r\n");
199 }
200}
201
207static void cmd_totp_add(const char* args) {
208 static const char* usage =
209 "Usage: TOTP ADD <type:totp|hotp> <name> <secret> [issuer] [digits] [period] [algo] [counter]\r\n";
210 char typeBuf[8] = {};
211 char name[OathStore::NAME_LEN + 1] = {};
212 char secret[SECRET_B32_LEN] = {};
213 char issuer[OathStore::ISSUER_LEN + 1] = {};
214 char digitsBuf[8] = {};
215 char periodBuf[8] = {};
216 char algoBuf[8] = {};
217 char counterBuf[24] = {};
218
219 const char* p = nextToken(args, typeBuf, sizeof(typeBuf));
220 if (!p || !*typeBuf) {
221 cdc::serial::Console::printf("%s", usage);
222 return;
223 }
224 p = nextToken(p, name, sizeof(name));
225 if (!p || !*name) {
226 cdc::serial::Console::printf("%s", usage);
227 return;
228 }
229 p = nextToken(p, secret, sizeof(secret));
230 if (!p || !*secret) {
231 cdc::serial::Console::printf("%s", usage);
232 return;
233 }
234 p = nextToken(p, issuer, sizeof(issuer));
235 p = nextToken(p, digitsBuf, sizeof(digitsBuf));
236 p = nextToken(p, periodBuf, sizeof(periodBuf));
237 p = nextToken(p, algoBuf, sizeof(algoBuf));
238 p = nextToken(p, counterBuf, sizeof(counterBuf));
239
240 uint8_t type = parseType(typeBuf);
241 uint8_t digits = digitsBuf[0] ? static_cast<uint8_t>(atoi(digitsBuf)) : OathStore::DEFAULT_DIGITS;
242 uint32_t period = periodBuf[0] ? static_cast<uint32_t>(atoi(periodBuf)) : OathStore::DEFAULT_PERIOD;
243 uint8_t algo = parseAlgo(algoBuf);
244 uint64_t counter = counterBuf[0] ? strtoull(counterBuf, nullptr, 10) : 0;
245
247 type,
248 name,
249 issuer[0] ? issuer : nullptr,
250 secret,
251 digits,
252 period,
253 algo,
254 counter
255 );
256 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
257}
258
263static void cmd_totp_del(const char* args) {
264 if (!args || !*args) {
265 cdc::serial::Console::printf("Usage: TOTP DEL <index>\r\n");
266 return;
267 }
268 uint16_t index = static_cast<uint16_t>(atoi(args));
269 uint16_t slot = 0;
270 if (!findSlotByIndex(index, &slot)) {
271 cdc::serial::Console::printf("ERROR: index not found\r\n");
272 return;
273 }
274 bool ok = OathStore::instance().deleteAccount(slot);
275 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
276}
277
285static void cmd_totp_get(const char* args) {
286 if (!args || !*args) {
287 cdc::serial::Console::printf("Usage: TOTP GET <index>\r\n");
288 return;
289 }
290 uint16_t index = static_cast<uint16_t>(atoi(args));
291 uint16_t slot = 0;
292 if (!findSlotByIndex(index, &slot)) {
293 cdc::serial::Console::printf("ERROR: index not found\r\n");
294 return;
295 }
296 OathEntry account = {};
297 if (!OathStore::instance().readAccount(slot, &account)) {
298 cdc::serial::Console::printf("ERROR: read failed\r\n");
299 return;
300 }
301 char code[9] = {};
302 int8_t remaining = OathStore::instance().generateCode(slot, code, sizeof(code));
303 if (remaining < 0) {
304 cdc::serial::Console::printf("ERROR: time not valid\r\n");
305 return;
306 }
307 const char* issuer = account.issuer;
308 if (account.type == static_cast<uint8_t>(OathType::HOTP)) {
309 // account.counter is the pre-increment value (read before generateCode ran);
310 // it is the moving factor that produced the displayed code.
311 if (issuer && issuer[0]) {
312 cdc::serial::Console::printf("%s (counter %llu) [%s]\r\n", code,
313 static_cast<unsigned long long>(account.counter), issuer);
314 } else {
315 cdc::serial::Console::printf("%s (counter %llu)\r\n", code,
316 static_cast<unsigned long long>(account.counter));
317 }
318 return;
319 }
320 if (issuer && issuer[0]) {
321 cdc::serial::Console::printf("%s (%ds) [%s]\r\n", code, remaining, issuer);
322 } else {
323 cdc::serial::Console::printf("%s (%ds)\r\n", code, remaining);
324 }
325}
326
334static int hexDecode(const char* hex, uint8_t* out, size_t outMax) {
335 if (!hex || !out) return -1;
336 size_t len = strlen(hex);
337 if (len == 0 || (len % 2) != 0) return -1;
338 size_t count = len / 2;
339 if (count > outMax) return -1;
340
341 auto nibble = [](char c) -> int {
342 if (c >= '0' && c <= '9') return c - '0';
343 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
344 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
345 return -1;
346 };
347 for (size_t i = 0; i < count; i++) {
348 int hi = nibble(hex[i * 2]);
349 int lo = nibble(hex[i * 2 + 1]);
350 if (hi < 0 || lo < 0) return -1;
351 out[i] = static_cast<uint8_t>((hi << 4) | lo);
352 }
353 return static_cast<int>(count);
354}
355
365static void cmd_chalresp(const char* args) {
366 char name[OathStore::NAME_LEN + 1] = {};
367 char hexBuf[2 * 128 + 2] = {};
368
369 const char* p = nextToken(args, name, sizeof(name));
370 if (!p || !*name) {
371 cdc::serial::Console::printf("Usage: CHALRESP <name> <hex-challenge>\r\n");
372 return;
373 }
374 p = nextToken(p, hexBuf, sizeof(hexBuf));
375 if (!*hexBuf) {
376 cdc::serial::Console::printf("Usage: CHALRESP <name> <hex-challenge>\r\n");
377 return;
378 }
379 // nextToken stops at the buffer limit but keeps consuming the rest of the
380 // token, silently dropping it. The buffer holds one slot beyond the 256 hex
381 // chars of a maximum 128-byte challenge, so a longer token is detectable and
382 // rejected instead of computing over a truncated input.
383 if (strlen(hexBuf) > 2 * 128) {
384 cdc::serial::Console::printf("ERROR: challenge too long (max 128 bytes)\r\n");
385 return;
386 }
387
388 uint8_t challenge[128] = {};
389 int clen = hexDecode(hexBuf, challenge, sizeof(challenge));
390 if (clen < 0) {
391 cdc::serial::Console::printf("ERROR: invalid hex challenge\r\n");
392 return;
393 }
394
397 name, challenge, static_cast<size_t>(clen), response, nullptr);
398 if (rlen <= 0) {
399 cdc::serial::Console::printf("ERROR: no CR entry named '%s'\r\n", name);
400 return;
401 }
402
403 char hexOut[2 * cdc::core::IChallengeResponder::MAX_RESPONSE_LEN + 1] = {};
404 for (int i = 0; i < rlen; i++) {
405 snprintf(hexOut + i * 2, 3, "%02x", response[i]);
406 }
407 cdc::serial::Console::printf("%s\r\n", hexOut);
408}
409
414 {"LIST", "", "List all 2FA entries", cmd_totp_list},
415 {"ADD", "<type> <name> <secret> [issuer] [digits] [period] [algo] [counter]","Add 2FA entry", cmd_totp_add},
416 {"DEL", "<index>", "Delete 2FA entry by index", cmd_totp_del},
417 {"GET", "<index>", "Generate code by index", cmd_totp_get},
418 {nullptr, nullptr, nullptr, nullptr},
419};
420
421static void cmd_totp(const char* args) {
423}
424
428static void registerCommands() {
429 if (s_commandsRegistered) return;
431 reg.registerCommand({"TOTP",
432 "2FA authenticator: LIST/ADD/DEL/GET (TOTP+HOTP)",
433 cmd_totp, CMD_MODULE, true, kTotpSubs});
434 reg.registerCommand({"CHALRESP",
435 "Challenge-response: <name> <hex-challenge> (CR entries)",
436 cmd_chalresp, CMD_MODULE, true, nullptr});
438}
439
441
443static void wizardEdit(uint16_t slot);
444
446public:
452 void init(uint16_t slot, const char* name) {
453 slot_ = slot;
454 strncpy(name_, name ? name : "", sizeof(name_) - 1);
455 name_[sizeof(name_) - 1] = '\0';
456 issuer_[0] = '\0';
457 generated_ = false;
458 updateCode();
459 }
460
465 void onEnter(void* context) override {
466 (void)context;
467 generated_ = false;
468 updateCode();
469 if (isTotp_ && !timeValid_) {
470 ui::showToastError(ui::tr("mod_2fa.time_invalid"));
471 }
472 dirty_ = true;
473 }
474
481 void onResume() override {
482 generated_ = false;
483 updateCode();
484 dirty_ = true;
485 }
486
495 void onTick(uint32_t nowMs) override {
496 if (!isTotp_) return;
497 if (nowMs - lastUpdateMs_ >= 1000) {
498 lastUpdateMs_ = nowMs;
499 updateCode();
500 markDirty();
501 }
502 }
503
508 void render(bool partial) override {
509 auto* display = hal::getDisplayInstance();
510 if (!display) return;
511
512 auto* gfx = static_cast<Gdey029T94*>(display->getNativeHandle());
513 if (!gfx) return;
514
515 if (!partial) {
516 gfx->fillScreen(EPD_WHITE);
517 }
518
519 gfx->setTextColor(EPD_BLACK);
520 gfx->setTextSize(1);
521 gfx->setCursor(8, 6);
522 cdc::ui::render::printText(gfx, ui::tr("mod_2fa.code"));
523 gfx->drawFastHLine(0, 22, display->getWidth(), EPD_BLACK);
524
525 gfx->setCursor(8, 28);
526 cdc::ui::render::printText(gfx, name_);
527 if (issuer_[0]) {
528 gfx->setCursor(8, 40);
529 cdc::ui::render::printText(gfx, issuer_);
530 }
531
532 if (isCr_) {
533 gfx->setCursor(8, 60);
534 cdc::ui::render::printText(gfx, ui::tr("mod_2fa.cr_entry"));
535 clearDirty();
536 return;
537 }
538
539 if (isTotp_ && !timeValid_) {
540 gfx->setTextSize(1);
541 gfx->setCursor(8, 60);
542 cdc::ui::render::printText(gfx, ui::tr("mod_2fa.time_invalid"));
543 clearDirty();
544 return;
545 }
546
547 // Large centered code
548 gfx->setTextSize(2);
549 int16_t x1, y1;
550 uint16_t w, h;
551 gfx->getTextBounds(code_, 0, 0, &x1, &y1, &w, &h);
552 int16_t codeX = (display->getWidth() - w) / 2;
553 gfx->setCursor(codeX, 58);
554 gfx->print(code_);
555
556 gfx->setTextSize(1);
557 if (isTotp_) {
558 // Progress bar + remaining counter
559 const int barX = 20;
560 const int barY = 92;
561 const int barW = display->getWidth() - 40;
562 const int barH = 8;
563 gfx->drawRect(barX, barY, barW, barH, EPD_BLACK);
564 uint32_t period = (period_ == 0) ? 1 : period_;
565 uint32_t rem = (remaining_ > period) ? period : remaining_;
566 uint16_t fillW = static_cast<uint16_t>((barW - 2) * rem / period);
567 gfx->fillRect(barX + 1, barY + 1, fillW, barH - 2, EPD_BLACK);
568 gfx->setCursor(barX, barY + 14);
569 display->printf("%us", static_cast<unsigned>(remaining_));
570 } else {
571 // HOTP: show the counter that produced the current code.
572 char line[32];
573 snprintf(line, sizeof(line), "%s: %llu", ui::tr("mod_2fa.counter"),
574 static_cast<unsigned long long>(counter_));
575 gfx->setCursor(8, 92);
577 }
578
579 clearDirty();
580 }
581
587 ui::InputResult onKey(char key) override {
588 if (key == 'N') {
590 }
591 if (key == '3') {
592 wizardEdit(slot_);
594 }
595 if (key == '5' && !isTotp_ && !isCr_) {
596 // Advance to the next HOTP code on demand.
597 generated_ = false;
598 updateCode();
599 markDirty();
601 }
602 if (key == 'Y' && !isCr_) {
603 auto* kb = core::getKeyboard();
604 if (kb && kb->isConnected()) {
605 bool valid = isTotp_ ? (timeValid_ && code_[0] != '-') : code_[0] != '\0';
606 if (valid) {
607 kb->typeString(code_);
608 ui::showToastSuccess("Typed");
609 }
610 } else {
611 ui::showToastError(ui::tr("mod_2fa.no_keyboard"));
612 }
614 }
616 }
617
622 const char* getName() const override { return "OathCodeView"; }
623
628 const char* getFooterHint() const override {
629 if (isCr_) {
630 return ui::tr("mod_2fa.hint_edit");
631 }
632 if (!isTotp_) {
633 return ui::tr("mod_2fa.hint_hotp");
634 }
635 auto* kb = core::getKeyboard();
636 if (kb && kb->isConnected()) {
637 return ui::tr("mod_2fa.hint_type");
638 }
639 return ui::tr("mod_2fa.hint_edit");
640 }
641
642private:
649 void updateCode() {
651
652 OathEntry account = {};
653 if (!store.readAccount(slot_, &account)) {
654 isTotp_ = true;
655 isCr_ = false;
656 timeValid_ = false;
657 strncpy(code_, "------", sizeof(code_) - 1);
658 remaining_ = 0;
659 period_ = 0;
660 return;
661 }
662
663 isCr_ = account.type == static_cast<uint8_t>(OathType::CR);
664 isTotp_ = account.type == static_cast<uint8_t>(OathType::TOTP);
665 strncpy(issuer_, account.issuer, sizeof(issuer_) - 1);
666 issuer_[sizeof(issuer_) - 1] = '\0';
667 period_ = account.period;
668
669 if (isCr_) {
670 // CR has no displayable code; it is consumed via the transports.
671 code_[0] = '\0';
672 remaining_ = 0;
673 return;
674 }
675
676 if (!isTotp_) {
677 // HOTP: generate exactly once per explicit user action.
678 if (generated_) return;
679 counter_ = account.counter;
680 int8_t rc = store.generateCode(slot_, code_, sizeof(code_));
681 if (rc < 0) {
682 strncpy(code_, "------", sizeof(code_) - 1);
683 }
684 generated_ = true;
685 remaining_ = 0;
686 return;
687 }
688
689 timeValid_ = store.isTimeValid();
690 if (!timeValid_) {
691 strncpy(code_, "------", sizeof(code_) - 1);
692 remaining_ = 0;
693 return;
694 }
695 int8_t rem = store.generateCode(slot_, code_, sizeof(code_));
696 if (rem >= 0) {
697 remaining_ = static_cast<uint8_t>(rem);
698 } else {
699 timeValid_ = false;
700 strncpy(code_, "------", sizeof(code_) - 1);
701 remaining_ = 0;
702 }
703 }
704
705 uint16_t slot_ = 0;
706 char name_[OathStore::NAME_LEN + 1] = {};
707 char issuer_[OathStore::ISSUER_LEN + 1] = {};
708 char code_[9] = {};
709 uint8_t remaining_ = 0;
710 uint32_t period_ = 0;
711 uint64_t counter_ = 0;
712 bool isTotp_ = true;
713 bool isCr_ = false;
714 bool timeValid_ = false;
715 bool generated_ = false;
716 uint32_t lastUpdateMs_ = 0;
717};
718
720
731static bool s_viewsInitialized = false;
732
734static ui::ListItem* s_listItems = nullptr;
735static char (*s_listLabels)[24] = nullptr;
736static uint16_t* s_listSlots = nullptr;
737static uint16_t s_accountCount = 0;
738static uint16_t s_capacity = 0;
739
741 uint8_t type;
745 uint8_t digits;
746 uint8_t algorithm;
747 uint32_t period;
748 uint64_t counter;
749 uint8_t flags;
751 uint16_t editSlot;
752};
753
754EXT_RAM_BSS_ATTR static WizardState s_wizard = {};
755
763static void pushT9WizardStep(const char* title, const char* initialText,
764 uint16_t maxLen, ui::T9InputView::SaveCallback onSave) {
765 s_t9Input.init(title, initialText, maxLen);
766 s_t9Input.setOnSave(onSave);
768}
769
773static void freeListBuffers() {
774 delete[] s_listItems;
775 delete[] s_listLabels;
776 delete[] s_listSlots;
777 s_listItems = nullptr;
778 s_listLabels = nullptr;
779 s_listSlots = nullptr;
780 s_capacity = 0;
781 s_accountCount = 0;
782}
783
784static void rebuildList();
785static void onListSelect(uint16_t index, void* userData);
786static void wizardStart();
787static void wizardEdit(uint16_t slot);
788static void onWizardType(uint16_t index, void* userData);
789static void onWizardName(const char* text);
790static void onWizardSecret(const char* text);
791static void onWizardIssuer(const char* text);
792static void onWizardDigits(uint16_t index, void* userData);
793static void onWizardAlgo(uint16_t index, void* userData);
794static void onWizardPeriod(uint16_t index, void* userData);
795static void onWizardTouch(uint16_t index, void* userData);
796static void onWizardUsbCr(uint16_t index, void* userData);
797static void pushAlgoStep();
798static void pushTouchStep();
799static void pushUsbCrStep();
800static void wizardFinish();
801
809static void base32Encode(const uint8_t* data, size_t dataLen, char* out, size_t outMax) {
810 static const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
811 if (!out || outMax == 0) return;
812 size_t outPos = 0;
813 uint32_t buffer = 0;
814 uint8_t bitsLeft = 0;
815
816 for (size_t i = 0; i < dataLen; i++) {
817 buffer = (buffer << 8) | data[i];
818 bitsLeft += 8;
819 while (bitsLeft >= 5) {
820 uint8_t index = (buffer >> (bitsLeft - 5)) & 0x1F;
821 bitsLeft -= 5;
822 if (outPos + 1 >= outMax) {
823 out[outPos] = '\0';
824 return;
825 }
826 out[outPos++] = alphabet[index];
827 }
828 }
829
830 if (bitsLeft > 0) {
831 uint8_t index = (buffer << (5 - bitsLeft)) & 0x1F;
832 if (outPos + 1 < outMax) {
833 out[outPos++] = alphabet[index];
834 }
835 }
836 out[outPos] = '\0';
837}
838
843static bool ensureListBuffers() {
844 uint16_t cap = OathStore::instance().capacity();
845 if (cap == 0) return false;
846 if (cap == s_capacity && s_listItems && s_listLabels && s_listSlots) return true;
847
848 delete[] s_listItems;
849 delete[] s_listLabels;
850 delete[] s_listSlots;
851 s_listItems = nullptr;
852 s_listLabels = nullptr;
853 s_listSlots = nullptr;
854 s_capacity = 0;
855
856 s_listItems = new (std::nothrow) ui::ListItem[cap + 1];
857 s_listLabels = new (std::nothrow) char[cap][24];
858 s_listSlots = new (std::nothrow) uint16_t[cap];
859 if (!s_listItems || !s_listLabels || !s_listSlots) {
860 delete[] s_listItems;
861 delete[] s_listLabels;
862 delete[] s_listSlots;
863 s_listItems = nullptr;
864 s_listLabels = nullptr;
865 s_listSlots = nullptr;
866 s_capacity = 0;
867 return false;
868 }
869 s_capacity = cap;
870 return true;
871}
872
876static void rebuildList() {
877 if (!ensureListBuffers()) {
879 "2FA list allocation failed");
880 return;
881 }
882 s_accountCount = 0;
883 s_listItems[0] = {ui::tr("mod_2fa.add_account"), 0, false, nullptr};
884
885 auto cb = [](uint16_t slot, const cdc::core::TropicStorage::CacheEntry& entry, void* user) {
886 (void)user;
887 if (s_accountCount >= s_capacity) return;
888 uint16_t logical = 0;
889 if (!OathStore::instance().toLogicalSlot(slot, &logical)) return;
890 strncpy(s_listLabels[s_accountCount], entry.name, sizeof(s_listLabels[s_accountCount]) - 1);
892 s_listSlots[s_accountCount] = logical;
893 uint16_t idx = static_cast<uint16_t>(s_accountCount + 1);
895 s_listItems[idx].icon = 0;
896 s_listItems[idx].iconDisabled = false;
897 s_listItems[idx].userData = reinterpret_cast<void*>(static_cast<uintptr_t>(logical));
899 };
900
903 OathStore::instance().rmemStart(),
904 OathStore::instance().rmemEnd(),
905 cb, nullptr);
906
907 s_listView.init(ui::tr("mod_2fa.title"), s_listItems, static_cast<uint16_t>(s_accountCount + 1));
908}
909
915static void onListSelect(uint16_t index, void* userData) {
916 (void)userData;
917 if (index == 0) {
918 wizardStart();
919 return;
920 }
921 if (index - 1 >= s_accountCount) return;
922
923 uint16_t slot = s_listSlots[index - 1];
924 const char* name = s_listLabels[index - 1];
925 s_codeView.init(slot, name);
927}
928
932static void wizardStart() {
933 memset(&s_wizard, 0, sizeof(s_wizard));
934 s_wizard.type = static_cast<uint8_t>(OathType::TOTP);
936 s_wizard.algorithm = static_cast<uint8_t>(OathAlgorithm::SHA1);
938 s_wizard.counter = 0;
939 s_wizard.flags = 0;
940 s_wizard.editMode = false;
941 s_wizard.editSlot = 0;
942
943 static ui::ListItem typeItems[3] = {
944 {"TOTP", 0, false, nullptr},
945 {"HOTP", 0, false, nullptr},
946 {"CR", 0, false, nullptr}
947 };
948 s_typeMenu.setOnSelect(onWizardType);
949 s_typeMenu.init(ui::tr("mod_2fa.type"), typeItems, 3);
951}
952
957static void wizardEdit(uint16_t slot) {
958 OathEntry account = {};
959 if (!OathStore::instance().readAccount(slot, &account)) {
960 ui::showToastError(ui::tr("core.failed"));
961 return;
962 }
963
964 memset(&s_wizard, 0, sizeof(s_wizard));
965 s_wizard.type = account.type;
966 strncpy(s_wizard.name, account.name, sizeof(s_wizard.name) - 1);
967 strncpy(s_wizard.issuer, account.issuer, sizeof(s_wizard.issuer) - 1);
968 s_wizard.digits = account.digits;
969 s_wizard.algorithm = account.algorithm;
970 s_wizard.period = account.period;
971 s_wizard.counter = account.counter;
972 s_wizard.flags = account.flags;
973 s_wizard.editMode = true;
974 s_wizard.editSlot = slot;
975 base32Encode(account.secret, account.secretLen, s_wizard.secret, sizeof(s_wizard.secret));
976
977 // The entry type stays fixed when editing; jump straight to the name step.
978 pushT9WizardStep(ui::tr("mod_2fa.account_name"), s_wizard.name, OathStore::NAME_LEN, onWizardName);
979}
980
986static void onWizardType(uint16_t index, void* userData) {
987 (void)userData;
988 switch (index) {
989 case 1: s_wizard.type = static_cast<uint8_t>(OathType::HOTP); break;
990 case 2: s_wizard.type = static_cast<uint8_t>(OathType::CR); break;
991 default: s_wizard.type = static_cast<uint8_t>(OathType::TOTP); break;
992 }
993 pushT9WizardStep(ui::tr("mod_2fa.account_name"), nullptr, OathStore::NAME_LEN, onWizardName);
994}
995
1000static void onWizardName(const char* text) {
1001 strncpy(s_wizard.name, text ? text : "", sizeof(s_wizard.name) - 1);
1002 pushT9WizardStep(ui::tr("mod_2fa.secret"), s_wizard.secret, SECRET_B32_LEN - 1, onWizardSecret);
1003}
1004
1009static void onWizardSecret(const char* text) {
1010 strncpy(s_wizard.secret, text ? text : "", sizeof(s_wizard.secret) - 1);
1011 // CR has no issuer/digits/period: jump straight to the algorithm step.
1012 if (s_wizard.type == static_cast<uint8_t>(OathType::CR)) {
1013 pushAlgoStep();
1014 return;
1015 }
1017}
1018
1023static void onWizardIssuer(const char* text) {
1024 strncpy(s_wizard.issuer, text ? text : "", sizeof(s_wizard.issuer) - 1);
1025
1026 static ui::ListItem digitsItems[3] = {
1027 {"6", 0, false, nullptr},
1028 {"7", 0, false, nullptr},
1029 {"8", 0, false, nullptr}
1030 };
1031 s_digitsMenu.setOnSelect(onWizardDigits);
1032 s_digitsMenu.init(ui::tr("mod_2fa.digits"), digitsItems, 3);
1034}
1035
1041static void onWizardDigits(uint16_t index, void* userData) {
1042 (void)userData;
1043 static const uint8_t digitMap[3] = {6, 7, 8};
1044 s_wizard.digits = digitMap[index % 3];
1045 pushAlgoStep();
1046}
1047
1055static void pushAlgoStep() {
1056 static ui::ListItem algoItems[3] = {
1057 {"SHA1", 0, false, nullptr},
1058 {"SHA256", 0, false, nullptr},
1059 {"SHA512", 0, false, nullptr}
1060 };
1061 bool isCr = s_wizard.type == static_cast<uint8_t>(OathType::CR);
1062 s_algoMenu.setOnSelect(onWizardAlgo);
1063 s_algoMenu.init(ui::tr("mod_2fa.algorithm"), algoItems, isCr ? 2 : 3);
1065}
1066
1072static void onWizardAlgo(uint16_t index, void* userData) {
1073 (void)userData;
1074 s_wizard.algorithm = static_cast<uint8_t>(index % 3);
1075
1076 if (s_wizard.type == static_cast<uint8_t>(OathType::CR)) {
1077 // CR has no period; offer the touch-confirm toggle, then finalize.
1078 pushTouchStep();
1079 return;
1080 }
1081
1082 if (s_wizard.type == static_cast<uint8_t>(OathType::HOTP)) {
1083 // HOTP has no period; finalize directly (counter stays from edit/default).
1084 wizardFinish();
1085 return;
1086 }
1087
1088 static ui::ListItem periodItems[2] = {
1089 {"30s", 0, false, nullptr},
1090 {"60s", 0, false, nullptr}
1091 };
1092 s_periodMenu.setOnSelect(onWizardPeriod);
1093 s_periodMenu.init(ui::tr("mod_2fa.period"), periodItems, 2);
1095}
1096
1102static void onWizardPeriod(uint16_t index, void* userData) {
1103 (void)userData;
1104 s_wizard.period = (index == 0) ? 30 : 60;
1105 wizardFinish();
1106}
1107
1114static void pushTouchStep() {
1115 static ui::ListItem touchItems[2] = {};
1116 touchItems[0] = {ui::tr("mod_2fa.touch_on"), 0, false, nullptr};
1117 touchItems[1] = {ui::tr("mod_2fa.touch_off"), 0, false, nullptr};
1118 s_touchMenu.setOnSelect(onWizardTouch);
1119 s_touchMenu.init(ui::tr("mod_2fa.touch"), touchItems, 2);
1121}
1122
1128static void onWizardTouch(uint16_t index, void* userData) {
1129 (void)userData;
1130 if (index == 0) {
1132 } else {
1133 s_wizard.flags &= ~OathFlag::TOUCH_REQUIRED;
1134 }
1135 pushUsbCrStep();
1136}
1137
1144static void pushUsbCrStep() {
1145 static ui::ListItem usbCrItems[2] = {};
1146 usbCrItems[0] = {ui::tr("mod_2fa.usb_cr_on"), 0, false, nullptr};
1147 usbCrItems[1] = {ui::tr("mod_2fa.usb_cr_off"), 0, false, nullptr};
1148 s_usbCrMenu.setOnSelect(onWizardUsbCr);
1149 s_usbCrMenu.init(ui::tr("mod_2fa.usb_cr"), usbCrItems, 2);
1151}
1152
1158static void onWizardUsbCr(uint16_t index, void* userData) {
1159 (void)userData;
1160 if (index == 0) {
1162 } else {
1163 s_wizard.flags &= ~OathFlag::USB_CR_SLOT;
1164 }
1165 wizardFinish();
1166}
1167
1171static void wizardFinish() {
1172 if (strlen(s_wizard.name) == 0 || strlen(s_wizard.secret) == 0) {
1173 ui::showToastError(ui::tr("mod_2fa.invalid_input"));
1175 return;
1176 }
1177
1178 bool ok = false;
1179 if (s_wizard.editMode) {
1181 s_wizard.editSlot,
1182 s_wizard.type,
1183 s_wizard.name,
1184 strlen(s_wizard.issuer) > 0 ? s_wizard.issuer : nullptr,
1185 s_wizard.secret,
1186 s_wizard.digits,
1187 s_wizard.period,
1188 s_wizard.algorithm,
1189 s_wizard.counter,
1190 s_wizard.flags
1191 );
1192 } else {
1194 s_wizard.type,
1195 s_wizard.name,
1196 strlen(s_wizard.issuer) > 0 ? s_wizard.issuer : nullptr,
1197 s_wizard.secret,
1198 s_wizard.digits,
1199 s_wizard.period,
1200 s_wizard.algorithm,
1201 s_wizard.counter,
1202 s_wizard.flags
1203 );
1204 }
1205
1206 if (ok) {
1207 // Enforce a single USB-CR responder: when this entry was designated,
1208 // demote any previously designated entry.
1209 if (s_wizard.flags & OathFlag::USB_CR_SLOT) {
1210 uint16_t slot = 0;
1211 if (OathStore::instance().findByName(s_wizard.name, &slot)) {
1213 }
1214 }
1215 ui::showToastSuccess(ui::tr("core.ok"));
1216 rebuildList();
1218 } else {
1219 ui::showToastError(ui::tr("core.failed"));
1220 }
1221}
1222
1228 static TwoFaModule inst;
1229 return inst;
1230}
1231
1237 LOG_I(TAG, "Initializing 2FA module");
1240
1242 if (slotRange_.hasRmem) {
1243 OathStore::instance().setSlotRange(slotRange_);
1245 } else {
1246 core::ModuleRegistry::instance().reportModuleError(getName(), "2FA slot range missing");
1248 return false;
1249 }
1250
1251 // Offer the challenge-response service for transport modules (USB OTP-HID,
1252 // BLE). The BLE CR transport itself lives in this module.
1255 if (!ble_chalresp_init()) {
1256 LOG_W(TAG, "BLE CR init failed (BLE might not be available)");
1257 }
1258
1260 return true;
1261}
1262
1269 return false;
1270 }
1272 return true;
1273}
1274
1279void TwoFaModule::onTick(uint32_t nowMs) {
1280 ble_chalresp_tick(nowMs);
1281}
1282
1295int TwoFaModule::challengeResponse(const char* entryName, const uint8_t* challenge,
1296 size_t clen, uint8_t* out) {
1297 return OathStore::instance().challengeResponse(entryName, challenge, clen, out, nullptr);
1298}
1299
1312int TwoFaModule::challengeResponseUsbSlot(const uint8_t* challenge, size_t clen,
1313 uint8_t* out, bool* touchRequiredOut) {
1314 return OathStore::instance().challengeResponseUsbSlot(challenge, clen, out, touchRequiredOut);
1315}
1316
1323 ModuleBase::stop();
1324}
1325
1331 slotRange_ = range;
1332}
1333
1340 req.mapName = getName();
1341 req.minRmemSlots = 1;
1342 return req;
1343}
1344
1351uint8_t TwoFaModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
1352 if (!items || maxItems == 0) return 0;
1353
1354 items[0] = {ui::tr("mod_2fa.title"), 50, []() -> ui::IView* {
1355 if (!s_viewsInitialized) {
1356 s_listView.setOnSelect(onListSelect);
1357 s_viewsInitialized = true;
1358 }
1359 rebuildList();
1360 return &s_listView;
1361 }, nullptr, getName(), core::MenuLocation::MAIN_MENU, nullptr};
1362
1363 return 1;
1364}
1365
1367static constexpr int kSchemaVer = 1;
1368
1370static constexpr size_t kBase32BufLen = 103 + 1; // ceil(64*8/5) = 103 chars + null
1371
1384 if (!out) return false;
1385
1386 auto& store = OathStore::instance();
1387 if (!store.hasSlotRange()) return false;
1388
1389 cJSON_AddNumberToObject(out, "schema_ver", kSchemaVer);
1390 cJSON* entries = cJSON_AddArrayToObject(out, "entries");
1391 if (!entries) return false;
1392
1393 struct ExportCtx {
1394 cJSON* arr;
1395 uint16_t count;
1396 } ctx = { entries, 0 };
1397
1398 auto cb = [](uint16_t slot, const cdc::core::TropicStorage::CacheEntry&, void* user) {
1399 auto* c = static_cast<ExportCtx*>(user);
1400 auto& store = OathStore::instance();
1401
1402 uint16_t logical = 0;
1403 if (!store.toLogicalSlot(slot, &logical)) return;
1404
1405 OathEntry entry = {};
1406 if (!store.readAccount(logical, &entry)) return;
1407
1408 char b32[kBase32BufLen] = {};
1409 base32Encode(entry.secret, entry.secretLen, b32, sizeof(b32));
1410
1411 cJSON* obj = cJSON_CreateObject();
1412 if (!obj) return;
1413
1414 cJSON_AddStringToObject(obj, "name", entry.name);
1415 cJSON_AddStringToObject(obj, "issuer", entry.issuer);
1416 cJSON_AddNumberToObject(obj, "type", entry.type);
1417 cJSON_AddNumberToObject(obj, "algorithm", entry.algorithm);
1418 cJSON_AddNumberToObject(obj, "digits", entry.digits);
1419 cJSON_AddNumberToObject(obj, "period", entry.period);
1420 // counter stored as a double; at 53 bits of mantissa this covers
1421 // all meaningful HOTP counters without loss.
1422 cJSON_AddNumberToObject(obj, "counter", static_cast<double>(entry.counter));
1423 cJSON_AddNumberToObject(obj, "flags", entry.flags);
1424 cJSON_AddStringToObject(obj, "secret", b32);
1425
1426 cJSON_AddItemToArray(c->arr, obj);
1427 c->count++;
1428 };
1429
1431 store.moduleId(),
1432 store.rmemStart(),
1433 store.rmemEnd(),
1434 cb, &ctx);
1435
1436 return ctx.count > 0;
1437}
1438
1450static bool importOathEntry(const cJSON* entry, void* user) {
1451 (void)user;
1452 if (!cJSON_IsObject(entry)) return false;
1453
1454 const cJSON* jName = cJSON_GetObjectItemCaseSensitive(entry, "name");
1455 const cJSON* jIssuer = cJSON_GetObjectItemCaseSensitive(entry, "issuer");
1456 const cJSON* jType = cJSON_GetObjectItemCaseSensitive(entry, "type");
1457 const cJSON* jAlgo = cJSON_GetObjectItemCaseSensitive(entry, "algorithm");
1458 const cJSON* jDigits = cJSON_GetObjectItemCaseSensitive(entry, "digits");
1459 const cJSON* jPeriod = cJSON_GetObjectItemCaseSensitive(entry, "period");
1460 const cJSON* jCounter = cJSON_GetObjectItemCaseSensitive(entry, "counter");
1461 const cJSON* jFlags = cJSON_GetObjectItemCaseSensitive(entry, "flags");
1462 const cJSON* jSecret = cJSON_GetObjectItemCaseSensitive(entry, "secret");
1463
1464 if (!cJSON_IsString(jName) || !jName->valuestring || jName->valuestring[0] == '\0' ||
1465 !cJSON_IsString(jSecret) || !jSecret->valuestring || jSecret->valuestring[0] == '\0' ||
1466 !cJSON_IsNumber(jType) ||
1467 !cJSON_IsNumber(jAlgo) ||
1468 !cJSON_IsNumber(jDigits) ||
1469 !cJSON_IsNumber(jPeriod)) {
1470 LOG_W(TAG, "2FA import: skipping malformed entry");
1471 return false;
1472 }
1473
1474 const char* name = jName->valuestring;
1475 const char* issuer = (cJSON_IsString(jIssuer) && jIssuer->valuestring) ? jIssuer->valuestring : nullptr;
1476 const char* secret = jSecret->valuestring;
1477 uint8_t type = static_cast<uint8_t>(jType->valuedouble);
1478 uint8_t algorithm = static_cast<uint8_t>(jAlgo->valuedouble);
1479 uint8_t digits = static_cast<uint8_t>(jDigits->valuedouble);
1480 uint32_t period = static_cast<uint32_t>(jPeriod->valuedouble);
1481 uint64_t counter = cJSON_IsNumber(jCounter)
1482 ? static_cast<uint64_t>(jCounter->valuedouble)
1483 : 0;
1484 uint8_t flags = cJSON_IsNumber(jFlags)
1485 ? static_cast<uint8_t>(jFlags->valuedouble)
1486 : 0;
1487
1488 auto& store = OathStore::instance();
1489 uint16_t existingSlot = 0;
1490 bool ok;
1491 if (store.findByName(name, &existingSlot)) {
1492 ok = store.updateAccount(existingSlot, type, name, issuer,
1493 secret, digits, period, algorithm, counter, flags);
1494 } else {
1495 ok = store.addAccount(type, name, issuer, secret, digits, period, algorithm, counter, flags);
1496 }
1497
1498 if (!ok) {
1499 LOG_W(TAG, "2FA import: failed to store entry '%s'", name);
1500 }
1501 return ok;
1502}
1503
1514 if (!in) return {};
1515
1516 const cJSON* schemaVer = cJSON_GetObjectItemCaseSensitive(in, "schema_ver");
1517 if (cJSON_IsNumber(schemaVer) && static_cast<int>(schemaVer->valuedouble) != kSchemaVer) {
1518 LOG_W(TAG, "2FA backup schema_ver %d != expected %d, skipping",
1519 static_cast<int>(schemaVer->valuedouble), kSchemaVer);
1520 return {};
1521 }
1522
1523 const cJSON* entries = cJSON_GetObjectItemCaseSensitive(in, "entries");
1524 return cdc::ui::importJsonArray(entries, importOathEntry, nullptr);
1525}
1526
1527} // namespace cdc::mod_2fa
1528
1532extern "C" void mod_2fa_register() {
1534 auto& module = cdc::mod_2fa::TwoFaModule::instance();
1535 module.init();
1536 });
1537}
static const char * TAG
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
uint8_t flags
uint8_t moduleId
void mod_2fa_register()
Registers 2FA module initializer in the global module registry.
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
Challenge-response provider interface.
static constexpr size_t MAX_RESPONSE_LEN
Largest possible raw HMAC response (SHA256). Callers size out to this.
const char * getName() const override
Returns the module name supplied to the constructor.
Definition ModuleBase.h:32
ServiceState state_
Definition ModuleBase.h:67
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.
bool provide(ServiceType type, T *service)
static ServiceRegistry & instance()
Returns singleton service registry instance.
static TropicStorage & instance()
Returns singleton instance of TROPIC metadata cache manager.
bool forEachSlot(uint8_t moduleId, SlotCallback cb, void *ctx)
Iterates all cached slots for one module across its allowed range.
void onEnter(void *context) override
Refreshes code state when the view is entered.
const char * getFooterHint() const override
Returns context-aware footer hint text.
void onTick(uint32_t nowMs) override
Updates the TOTP countdown/code once per second.
ui::InputResult onKey(char key) override
Handles key actions for back, edit, next (HOTP), and typing.
void init(uint16_t slot, const char *name)
Initializes the code view for a specific account slot.
const char * getName() const override
Returns the static view identifier.
void onResume() override
Refreshes code state when the view resumes.
void render(bool partial) override
Renders account metadata, code, and validity/progress UI.
int challengeResponseUsbSlot(const uint8_t *challenge, size_t clen, uint8_t *out, bool *touchRequiredOut=nullptr)
Computes the raw HMAC challenge-response for the USB-CR slot entry.
bool isTimeValid() const
Returns whether system time is considered valid for TOTP.
void clearUsbCrFlagExcept(uint16_t keepSlot)
Clears the USB-CR-slot flag on every entry except keepSlot.
int challengeResponse(const char *entryName, const uint8_t *challenge, size_t clen, uint8_t *out, bool *touchRequiredOut=nullptr)
Computes the raw HMAC challenge-response for a CR entry by name.
static constexpr uint8_t NAME_LEN
Definition OathStore.h:57
bool hasSlotRange() const
Definition OathStore.h:160
uint16_t rmemEnd() const
Definition OathStore.h:163
bool toLogicalSlot(uint16_t slot, uint16_t *logicalIndexOut) const
Definition OathStore.h:157
uint8_t moduleId() const
Definition OathStore.h:161
bool addAccount(uint8_t type, const char *name, const char *issuer, const char *secretBase32, uint8_t digits, uint32_t period, uint8_t algorithm, uint64_t counter, uint8_t flags=0)
Adds a new OATH entry from a Base32 secret.
static OathStore & instance()
Returns singleton OATH store instance.
static constexpr uint8_t DEFAULT_DIGITS
Definition OathStore.h:60
int8_t generateCode(uint16_t slot, char *codeOut, size_t codeOutLen)
Renders the current code for an entry into codeOut.
uint16_t capacity() const
Definition OathStore.h:153
bool updateAccount(uint16_t slot, uint8_t type, const char *name, const char *issuer, const char *secretBase32, uint8_t digits, uint32_t period, uint8_t algorithm, uint64_t counter, uint8_t flags=0)
Updates an existing OATH entry.
uint16_t rmemStart() const
Definition OathStore.h:162
static constexpr uint32_t DEFAULT_PERIOD
Definition OathStore.h:61
void setSlotRange(const cdc::core::IModule::SlotRange &range)
Configures logical-to-physical slot mapping for OATH entries.
static constexpr uint8_t ISSUER_LEN
Definition OathStore.h:58
bool findByName(const char *name, uint16_t *slotOut) const
Finds a logical slot index by account name.
bool readAccount(uint16_t slot, OathEntry *out)
Reads one OATH entry from secure-element storage.
bool deleteAccount(uint16_t slot)
Deletes account in logical slot.
bool exportBackup(cJSON *out) override
Exports all stored OATH entries into the module's backup section.
uint8_t getMenuItems(core::ModuleMenuItem *items, uint8_t maxItems) override
Provides main-menu entry for the 2FA module.
static TwoFaModule & instance()
Returns singleton 2FA module instance.
void stop() override
Stops the 2FA module and releases list buffers.
core::IModule::BackupResult importBackup(const cJSON *in) override
Restores OATH entries from the module's backup section.
int challengeResponseUsbSlot(const uint8_t *challenge, size_t clen, uint8_t *out, bool *touchRequiredOut) override
Computes the raw HMAC response for the designated USB-CR slot entry.
bool init() override
Initializes module resources, translations, commands, and slot mapping.
void setSlotRange(const core::IModule::SlotRange &range) override
Stores assigned Tropic slot range for the module.
int challengeResponse(const char *entryName, const uint8_t *challenge, size_t clen, uint8_t *out) override
Computes the raw HMAC challenge-response for a named CR entry.
core::IModule::SlotRequest getSlotRequest() const override
Declares minimum slot requirements for the 2FA module.
void onTick(uint32_t nowMs) override
Forwards the BLE CR state machine on the main task.
bool start() override
Starts the 2FA module service.
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
void(*)(const char *text) SaveCallback
Definition T9InputView.h:29
void clearDirty() override
Definition IView.h:187
void markDirty() override
Definition IView.h:186
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
void popToAnchor(IView *anchor)
Pops views until the specified anchor view is the current view.
void push(IView *view, void *context=nullptr)
bool valid
const char * skipSpaces(const char *s)
Advances over leading ASCII whitespace in a C string.
Definition StringUtils.h:13
IKeyboardProvider * getKeyboard()
const char * nextToken(const char *s, char *out, size_t outSize)
Extracts one whitespace-delimited token from a string.
Definition StringUtils.h:31
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
Per-entry flag bits stored in OathEntry::flags.
Definition OathStore.h:32
constexpr uint8_t USB_CR_SLOT
Designate this CR entry as the USB OTP-HID slot-2 responder.
Definition OathStore.h:36
constexpr uint8_t TOUCH_REQUIRED
Require an on-device touch confirmation before answering a CR request.
Definition OathStore.h:34
static void onWizardAlgo(uint16_t index, void *userData)
Saves selected algorithm; opens period step (TOTP) or finishes (HOTP).
static WizardState s_wizard
static void wizardEdit(uint16_t slot)
OATH code detail view implementation.
static uint8_t parseType(const char *token)
Parses a textual or numeric entry-type token.
static void cmd_chalresp(const char *args)
Serial command computing a raw challenge-response for a named CR entry.
static ui::ListView s_digitsMenu
static void freeListBuffers()
Frees dynamically allocated list buffers used by the account list.
static bool ensureListBuffers()
Ensures account list backing buffers are allocated for current capacity.
static constexpr const char * CMD_MODULE
Serial command handlers for 2FA module.
static void rebuildList()
Rebuilds the account list view content from Tropic storage cache.
static ui::ListItem * s_listItems
Dynamic list buffers released by freeListBuffers.
static bool findSlotByIndex(uint16_t index, uint16_t *slotOut)
Resolves a displayed list index to the logical OATH slot number.
static uint8_t parseAlgo(const char *token)
Parses textual or numeric algorithm identifiers into store values.
OathType
OATH entry type discriminator.
Definition OathStore.h:23
static void cmd_totp_list(const char *args)
Serial command handler printing all configured OATH entries.
void ble_chalresp_deinit()
Tears down the BLE CR subsystem and removes GATT callbacks.
static constexpr int kSchemaVer
Schema version written to and expected from the 2FA backup section.
static void onWizardType(uint16_t index, void *userData)
Saves selected entry type and opens the name step.
static void onWizardSecret(const char *text)
Saves wizard secret and opens issuer step.
static void pushAlgoStep()
Pushes the algorithm-selection step.
static ui::ListView s_listView
2FA module UI state.
static ui::ListView s_touchMenu
bool ble_chalresp_init()
BLE GATT challenge-response transport.
static ui::ListView s_periodMenu
static void cmd_totp(const char *args)
static constexpr size_t SECRET_B32_LEN
static void registerCommands()
Registers serial commands exposed by the 2FA module.
static bool importOathEntry(const cJSON *entry, void *user)
Maps and upserts one OATH entry from its JSON representation.
static void wizardStart()
Starts the add-account wizard with default values.
static void onWizardIssuer(const char *text)
Saves wizard issuer and opens digit-selection step.
static void wizardFinish()
Validates wizard data and persists account changes.
constexpr ui::I18nEntry kStrings[]
static void pushT9WizardStep(const char *title, const char *initialText, uint16_t maxLen, ui::T9InputView::SaveCallback onSave)
Pushes a configured T9 input step for the account wizard flow.
static ui::ListView s_typeMenu
const char * nextToken(const char *s, char *out, size_t outSize)
Extracts one whitespace-delimited token from a string.
Definition StringUtils.h:31
static void cmd_totp_add(const char *args)
Serial command handler adding an OATH entry from tokens.
static void onListSelect(uint16_t index, void *userData)
Handles selection from the account list.
static void onWizardPeriod(uint16_t index, void *userData)
Saves selected period and finalizes add/edit operation.
static const cdc::serial::SubCommand kTotpSubs[]
Sub-command table for the TOTP serial command group.
static bool s_commandsRegistered
static void onWizardTouch(uint16_t index, void *userData)
Saves the CR touch-confirm choice and finalizes the entry.
static void cmd_totp_get(const char *args)
Serial command handler generating one code by index.
static uint16_t s_capacity
static OathCodeView s_codeView
static void onWizardUsbCr(uint16_t index, void *userData)
Saves the USB-CR-slot designation and finalizes the entry.
static constexpr size_t kBase32BufLen
Maximum Base32 string length for the longest supported secret (64 bytes raw).
static void pushTouchStep()
Pushes the CR touch-confirm toggle step.
static int hexDecode(const char *hex, uint8_t *out, size_t outMax)
Decodes a hex string into bytes.
static void onWizardDigits(uint16_t index, void *userData)
Saves selected code length and opens algorithm-selection step.
static void registerStrings()
static void onWizardName(const char *text)
Saves wizard account name and opens secret step.
static bool s_viewsInitialized
static ui::T9InputView s_t9Input
static ui::ListView s_usbCrMenu
static char(* s_listLabels)[24]
static uint16_t * s_listSlots
static ui::ListView s_algoMenu
static uint16_t s_accountCount
static void base32Encode(const uint8_t *data, size_t dataLen, char *out, size_t outMax)
Encodes binary secret bytes into unpadded Base32 text.
static void cmd_totp_del(const char *args)
Serial command handler deleting an OATH entry by index.
void ble_chalresp_tick(uint32_t nowMs)
Main-task tick: processes a pending challenge (confirm + notify).
static void pushUsbCrStep()
Pushes the CR USB-slot-2 designation step.
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
void printText(Gdey029T94 *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:208
cdc::core::IModule::BackupResult importJsonArray(const cJSON *array, BackupEntryHandler handler, void *user)
Iterates a JSON backup array best-effort and tallies the outcome.
InputResult
Definition IView.h:10
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.
Per-module restore outcome reported by importBackup().
Definition IModule.h:85
Menu item registered by a module.
Definition IModule.h:29
Unified OATH credential record (TOTP, HOTP, and reserved CR).
Definition OathStore.h:42
uint8_t digits
Output digit count (TOTP/HOTP).
Definition OathStore.h:49
uint8_t secret[64]
Raw HMAC key.
Definition OathStore.h:46
uint8_t algorithm
OathAlgorithm value.
Definition OathStore.h:48
uint8_t secretLen
Valid bytes in secret.
Definition OathStore.h:47
uint64_t counter
Moving factor (HOTP only).
Definition OathStore.h:51
uint8_t type
OathType discriminator.
Definition OathStore.h:43
uint32_t period
TOTP step in seconds (TOTP only).
Definition OathStore.h:50
char issuer[32+1]
Optional issuer text.
Definition OathStore.h:45
char name[16+1]
Account label.
Definition OathStore.h:44
uint8_t flags
Reserved entry flags.
Definition OathStore.h:52
char name[OathStore::NAME_LEN+1]
char secret[SECRET_B32_LEN]
char issuer[OathStore::ISSUER_LEN+1]
Single English translation entry.
Definition I18n.h:44