CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
PasswordModule.cpp
Go to the documentation of this file.
8#include "cJSON.h"
10#include "esp_random.h"
11#include "cdc_ui/I18n.h"
12#include "cdc_ui/ViewStack.h"
13#include "cdc_views/ListView.h"
16#include "cdc_views/InfoView.h"
18#include "cdc_views/ToastView.h"
21#include "serial_cmd/Console.h"
22#include "cdc_log.h"
23#include "esp_attr.h"
24#include <cctype>
25#include <cstring>
26#include <strings.h>
27#include <new>
28#include <memory>
29#include <cstdio>
30
31static const char* TAG = "PASSWORD";
32
33namespace cdc::mod_password {
34
35constexpr ui::I18nEntry kStrings[] = {
36 {"mod_password.title", "Passwords"},
37 {"mod_password.new_entry", "New Entry"},
38 {"mod_password.field_title", "Title"},
39 {"mod_password.username", "Username"},
40 {"mod_password.password", "Password"},
41 {"mod_password.url", "URL"},
42 {"mod_password.totp_slot", "TOTP Slot (optional)"},
43 {"mod_password.notes", "Notes"},
44 {"mod_password.view", "View"},
45 {"mod_password.edit", "Edit"},
46 {"mod_password.actions", "Actions"},
47 {"mod_password.invalid_input", "Invalid input"},
48 {"mod_password.slot_error", "Slot map error"},
49 {"mod_password.hint_list", "[Y] View [3] Menu [N] Back"},
50 {"mod_password.confirm_delete", "Delete entry?"},
51 {"mod_password.hint_type", "[Y] Type [2/8] Scroll [N] Back"},
52 {"mod_password.no_keyboard", "No keyboard connected"},
53};
54
58
60
61static constexpr const char* CMD_MODULE = "password";
62static bool s_commandsRegistered = false;
63
66
72static bool isValidSlot(uint16_t slot) {
73 auto& store = PasswordStore::instance();
74 return store.hasSlotRange() && slot < store.capacity();
75}
76
81static void cmd_password_list(const char* args) {
82 (void)args;
83 auto& store = PasswordStore::instance();
84 if (!store.hasSlotRange()) {
85 cdc::serial::Console::printf("ERROR: slot map not configured\r\n");
86 return;
87 }
88 uint16_t cap = store.capacity();
89 if (cap == 0) {
90 cdc::serial::Console::printf("(no entries)\r\n");
91 return;
92 }
93 auto list = std::unique_ptr<PasswordStore::EntryIndex[]>(new (std::nothrow) PasswordStore::EntryIndex[cap]);
94 if (!list) {
95 cdc::serial::Console::printf("ERROR: out of memory\r\n");
96 return;
97 }
98 uint16_t count = 0;
99 if (!store.listEntriesSorted(list.get(), cap, &count)) {
100 cdc::serial::Console::printf("ERROR: list failed\r\n");
101 return;
102 }
103 if (count == 0) {
104 cdc::serial::Console::printf("(no entries)\r\n");
105 return;
106 }
107 for (uint16_t i = 0; i < count; i++) {
108 cdc::serial::Console::printf("slot %u: %s\r\n", list[i].slot, list[i].title);
109 }
110}
111
116static void cmd_password_get(const char* args) {
117 char slotBuf[8] = {};
118 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
119 if (!p || !slotBuf[0]) {
120 cdc::serial::Console::printf("Usage: PASSWORD GET <slot>\r\n");
121 return;
122 }
123 uint16_t slot = static_cast<uint16_t>(atoi(slotBuf));
124 if (!isValidSlot(slot)) {
125 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
126 return;
127 }
128 PasswordEntry entry = {};
129 if (!PasswordStore::instance().readEntry(slot, &entry)) {
130 cdc::serial::Console::printf("ERROR: empty slot or read failed\r\n");
131 return;
132 }
133 cdc::serial::Console::printf("Title: %s\r\n", entry.title);
134 cdc::serial::Console::printf("Username: %s\r\n", entry.username);
135 cdc::serial::Console::printf("Password: %s\r\n", entry.password);
136 cdc::serial::Console::printf("URL: %s\r\n", entry.url);
138 cdc::serial::Console::printf("TOTP Slot: none\r\n");
139 } else {
140 cdc::serial::Console::printf("TOTP Slot: %u\r\n", entry.totpSlot);
141 }
142 cdc::serial::Console::printf("Notes: %s\r\n", entry.notes);
143}
144
149static bool isPlaceholder(const char* s) {
150 return s && s[0] && s[1] == '\0' && (s[0] == 'x' || s[0] == 'X' || s[0] == '-');
151}
152
157static void generateRandomPassword(char* out, size_t outSize) {
158 static const char charset[] =
159 "abcdefghijklmnopqrstuvwxyz"
160 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
161 "0123456789"
162 "$!%=";
163 constexpr size_t charsetLen = sizeof(charset) - 1;
164 constexpr uint8_t charCount = 16;
165 if (outSize < charCount + 1) return;
166
167 uint8_t rand[charCount] = {};
169 bool gotRand = se && se->getRandom(rand, sizeof(rand));
170 if (!gotRand) {
171 for (uint8_t i = 0; i < charCount; i++) {
172 rand[i] = static_cast<uint8_t>(esp_random() & 0xFF);
173 }
174 }
175
176 for (uint8_t i = 0; i < charCount; i++) {
177 out[i] = charset[rand[i] % charsetLen];
178 }
179 out[charCount] = '\0';
180}
181
182static void cmd_password_add(const char* args) {
183 PasswordEntry entry = {};
185
186 char slotBuf[8] = {};
187 char title[PasswordStore::TITLE_LEN + 1] = {};
188 char username[PasswordStore::USERNAME_LEN + 1] = {};
189 char password[PasswordStore::PASSWORD_LEN + 1] = {};
190 char url[PasswordStore::URL_LEN + 1] = {};
191 char totpBuf[8] = {};
192
193 static constexpr const char* USAGE =
194 "Usage: PASSWORD ADD <slot|x> <title> <username|x> <password|x> <url|x> <totp|-> [notes]\r\n"
195 " <slot>: target RMEM slot, or 'x' for next free. Overwrites if occupied.\r\n"
196 " <password>: value, or 'x' to generate a random 16-char password.\r\n"
197 " <totp>: linked TOTP slot number, or '-' for no link.\r\n"
198 " Use 'x' for username/url to skip them. Use '\\\\ ' for spaces inside fields.\r\n";
199
200 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
201 if (!p || !slotBuf[0]) {
202 cdc::serial::Console::printf("%s", USAGE);
203 return;
204 }
205 p = nextToken(p, title, sizeof(title));
206 p = nextToken(p, username, sizeof(username));
207 p = nextToken(p, password, sizeof(password));
208 p = nextToken(p, url, sizeof(url));
209 p = nextToken(p, totpBuf, sizeof(totpBuf));
210 if (!title[0] || !username[0] || !password[0] || !url[0] || !totpBuf[0]) {
211 cdc::serial::Console::printf("%s", USAGE);
212 return;
213 }
214
215 const char* notes = skipSpaces(p);
216
217 auto& store = PasswordStore::instance();
218
219 uint16_t slot = 0;
220 if (isPlaceholder(slotBuf)) {
221 if (!store.findFreeLogicalSlot(&slot)) {
222 cdc::serial::Console::printf("ERROR: no free slots\r\n");
223 return;
224 }
225 } else {
226 slot = static_cast<uint16_t>(atoi(slotBuf));
227 if (!isValidSlot(slot)) {
228 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
229 return;
230 }
231 }
232
233 strncpy(entry.title, title, sizeof(entry.title) - 1);
234 if (!isPlaceholder(username)) {
235 strncpy(entry.username, username, sizeof(entry.username) - 1);
236 }
237
238 char generatedPassword[40] = {};
239 if (password[0] == 'x' && password[1] == '\0') {
240 generateRandomPassword(generatedPassword, sizeof(generatedPassword));
241 strncpy(entry.password, generatedPassword, sizeof(entry.password) - 1);
242 } else if (!isPlaceholder(password)) {
243 strncpy(entry.password, password, sizeof(entry.password) - 1);
244 }
245
246 if (!isPlaceholder(url)) {
247 strncpy(entry.url, url, sizeof(entry.url) - 1);
248 }
249
250 if (totpBuf[0] != '-' || totpBuf[1] != '\0') {
251 int totp = atoi(totpBuf);
252 if (totp < 0 || totp > 254) {
253 cdc::serial::Console::printf("ERROR: totp slot out of range (0-254 or '-')\r\n");
254 return;
255 }
256 entry.totpSlot = static_cast<uint8_t>(totp);
257 }
258
259 if (notes && notes[0]) {
260 strncpy(entry.notes, notes, sizeof(entry.notes) - 1);
262 }
263
264 bool ok = store.updateEntry(slot, entry);
265 if (ok) {
266 if (generatedPassword[0]) {
267 cdc::serial::Console::printf("Generated password: %s\r\n", generatedPassword);
268 }
269 cdc::serial::Console::printf("OK (slot %u)\r\n", slot);
270 } else {
271 cdc::serial::Console::printf("ERROR\r\n");
272 }
273}
274
280static void cmd_password_edit(const char* args) {
281 char slotBuf[8] = {};
282 char field[16] = {};
283 const char* usage =
284 "Usage: PASSWORD EDIT <slot> <title|username|password|url|totp|notes> <value>\r\n"
285 " Use '\\\\ ' for spaces inside values.\r\n";
286
287 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
288 if (!p || !slotBuf[0]) {
289 cdc::serial::Console::printf("%s", usage);
290 return;
291 }
292 p = nextToken(p, field, sizeof(field));
293 if (!field[0]) {
294 cdc::serial::Console::printf("%s", usage);
295 return;
296 }
297
298 uint16_t slot = static_cast<uint16_t>(atoi(slotBuf));
299 if (!isValidSlot(slot)) {
300 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
301 return;
302 }
303
304 PasswordEntry entry = {};
305 if (!PasswordStore::instance().readEntry(slot, &entry)) {
306 cdc::serial::Console::printf("ERROR: empty slot or read failed\r\n");
307 return;
308 }
309
310 const char* value = skipSpaces(p);
311 if (!value || !value[0]) {
312 cdc::serial::Console::printf("ERROR: empty value\r\n");
313 return;
314 }
315
316 if (strcasecmp(field, "title") == 0) {
317 if (strlen(value) > PasswordStore::TITLE_LEN) {
318 cdc::serial::Console::printf("ERROR: title too long (max %u)\r\n", PasswordStore::TITLE_LEN);
319 return;
320 }
321 memset(entry.title, 0, sizeof(entry.title));
322 strncpy(entry.title, value, sizeof(entry.title) - 1);
324 } else if (strcasecmp(field, "username") == 0) {
325 if (strlen(value) > PasswordStore::USERNAME_LEN) {
326 cdc::serial::Console::printf("ERROR: username too long (max %u)\r\n", PasswordStore::USERNAME_LEN);
327 return;
328 }
329 memset(entry.username, 0, sizeof(entry.username));
330 strncpy(entry.username, value, sizeof(entry.username) - 1);
332 } else if (strcasecmp(field, "password") == 0) {
333 if (strlen(value) > PasswordStore::PASSWORD_LEN) {
334 cdc::serial::Console::printf("ERROR: password too long (max %u)\r\n", PasswordStore::PASSWORD_LEN);
335 return;
336 }
337 memset(entry.password, 0, sizeof(entry.password));
338 strncpy(entry.password, value, sizeof(entry.password) - 1);
340 } else if (strcasecmp(field, "url") == 0) {
341 if (strlen(value) > PasswordStore::URL_LEN) {
342 cdc::serial::Console::printf("ERROR: url too long (max %u)\r\n", PasswordStore::URL_LEN);
343 return;
344 }
345 memset(entry.url, 0, sizeof(entry.url));
346 strncpy(entry.url, value, sizeof(entry.url) - 1);
348 } else if (strcasecmp(field, "totp") == 0) {
349 int totp = atoi(value);
350 if (totp < 0 || totp > 254) {
351 cdc::serial::Console::printf("ERROR: totp slot out of range (0-254)\r\n");
352 return;
353 }
354 entry.totpSlot = static_cast<uint8_t>(totp);
355 } else if (strcasecmp(field, "notes") == 0) {
356 if (strlen(value) > PasswordStore::NOTES_LEN) {
357 cdc::serial::Console::printf("ERROR: notes too long (max %u)\r\n", static_cast<unsigned>(PasswordStore::NOTES_LEN));
358 return;
359 }
360 memset(entry.notes, 0, sizeof(entry.notes));
361 strncpy(entry.notes, value, sizeof(entry.notes) - 1);
363 } else {
364 cdc::serial::Console::printf("%s", usage);
365 return;
366 }
367
368 bool ok = PasswordStore::instance().updateEntry(slot, entry);
369 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
370}
371
376static void cmd_password_del(const char* args) {
377 char slotBuf[8] = {};
378 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
379 if (!p || !slotBuf[0]) {
380 cdc::serial::Console::printf("Usage: PASSWORD DEL <slot>\r\n");
381 return;
382 }
383 uint16_t slot = static_cast<uint16_t>(atoi(slotBuf));
384 if (!isValidSlot(slot)) {
385 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
386 return;
387 }
388 bool ok = PasswordStore::instance().deleteEntry(slot);
389 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
390}
391
393 {"LIST", "", "List password entries (sorted by title)", cmd_password_list},
394 {"GET", "<slot>", "Show one entry by slot", cmd_password_get},
395 {"ADD", "<slot|x> <title> <user|x> <pw|x> <url|x> <totp|-> [notes]", "Add entry; 'x' for fields skips them", cmd_password_add},
396 {"EDIT", "<slot> <field> <value>", "Edit one field of an existing entry", cmd_password_edit},
397 {"DEL", "<slot>", "Delete entry by slot", cmd_password_del},
398 {nullptr, nullptr, nullptr, nullptr},
399};
400
401static void cmd_password(const char* args) {
403}
404
408static void registerCommands() {
409 if (s_commandsRegistered) return;
411
413 reg.registerCommand({"PASSWORD",
414 "Password vault: LIST/GET/ADD/EDIT/DEL",
416}
417
419
423static bool s_viewsInitialized = false;
424
425static ui::ListItem* s_listItems = nullptr;
427static uint16_t s_entryCount = 0;
428static uint16_t s_capacity = 0;
429
430static uint16_t s_activeSlot = 0;
431
437
438EXT_RAM_BSS_ATTR static WizardState s_wizard = {};
439
440static constexpr uint16_t NOTES_INPUT_MAX =
442 ? static_cast<uint16_t>(PasswordStore::NOTES_LEN)
443 : static_cast<uint16_t>(ui::T9InputView::MAX_TEXT_LEN);
444
448static void freeListBuffers() {
449 delete[] s_listItems;
450 delete[] s_entries;
451 s_listItems = nullptr;
452 s_entries = nullptr;
453 s_capacity = 0;
454 s_entryCount = 0;
455}
456
461static bool ensureListBuffers() {
462 uint16_t cap = PasswordStore::instance().capacity();
463 if (cap == 0) return false;
464 if (cap == s_capacity && s_listItems && s_entries) return true;
465
466 delete[] s_listItems;
467 delete[] s_entries;
468 s_listItems = nullptr;
469 s_entries = nullptr;
470 s_capacity = 0;
471
472 s_listItems = new (std::nothrow) ui::ListItem[cap + 1];
473 s_entries = new (std::nothrow) PasswordStore::EntryIndex[cap];
474 if (!s_listItems || !s_entries) {
475 delete[] s_listItems;
476 delete[] s_entries;
477 s_listItems = nullptr;
478 s_entries = nullptr;
479 s_capacity = 0;
480 return false;
481 }
482 s_capacity = cap;
483 return true;
484}
485
489static void rebuildList() {
490 if (!PasswordStore::instance().hasSlotRange()) {
491 ui::showToastError(ui::tr("mod_password.slot_error"));
492 return;
493 }
494 if (!ensureListBuffers()) {
496 "Password list allocation failed");
497 return;
498 }
499 s_entryCount = 0;
500 s_listItems[0] = {ui::tr("mod_password.new_entry"), 0, false, nullptr};
501
502 uint16_t count = 0;
504 s_entryCount = count;
505
506 for (uint16_t i = 0; i < s_entryCount; i++) {
507 uint16_t idx = static_cast<uint16_t>(i + 1);
508 s_listItems[idx].label = s_entries[i].title;
509 s_listItems[idx].icon = 0;
510 s_listItems[idx].iconDisabled = false;
511 s_listItems[idx].userData = reinterpret_cast<void*>(static_cast<uintptr_t>(s_entries[i].slot));
512 }
513
514 s_listView.init(ui::tr("mod_password.title"), s_listItems, static_cast<uint16_t>(s_entryCount + 1));
515s_listView.setHint(ui::tr("mod_password.hint_list"));
516}
517
520
525static void onTypePassword(void* userData) {
526 (void)userData;
527 auto* kb = core::getKeyboard();
528 if (kb && kb->isConnected()) {
529 if (s_passwordToType[0]) {
530 kb->typeString(s_passwordToType);
531 ui::showToastSuccess("Typed");
532 }
533 } else {
534 ui::showToastError(ui::tr("mod_password.no_keyboard"));
535 }
536}
537
542static void showDetails(uint16_t slot) {
543 PasswordEntry entry = {};
544 if (!PasswordStore::instance().readEntry(slot, &entry)) {
545 ui::showToastError(ui::tr("core.failed"));
546 return;
547 }
548
549 // Store password for type callback
550 strncpy(s_passwordToType, entry.password, sizeof(s_passwordToType) - 1);
551 s_passwordToType[sizeof(s_passwordToType) - 1] = '\0';
552
553 static EXT_RAM_BSS_ATTR char detailText[ui::InfoView::MAX_TEXT_LEN];
554 char totpBuf[16] = {};
555 const char* emptyText = ui::tr("core.empty");
556 char emptyWrapped[16] = {};
557 snprintf(emptyWrapped, sizeof(emptyWrapped), "(%s)", emptyText);
558 const char* totpText = emptyWrapped;
560 snprintf(totpBuf, sizeof(totpBuf), "%u", entry.totpSlot);
561 totpText = totpBuf;
562 }
563 const char* usernameText = entry.username[0] ? entry.username : emptyWrapped;
564 const char* passwordText = entry.password[0] ? entry.password : emptyWrapped;
565 const char* urlText = entry.url[0] ? entry.url : emptyWrapped;
566 const char* notesText = entry.notes[0] ? entry.notes : emptyWrapped;
567
568 snprintf(detailText, sizeof(detailText),
569 "Title: %s\n"
570 "Username: %s\n"
571 "Password: %s\n"
572 "URL: %s\n"
573 "TOTP Slot: %s\n"
574 "Notes: %s",
575 entry.title,
576 usernameText,
577 passwordText,
578 urlText,
579 totpText,
580 notesText);
581
582 s_infoView.init(ui::tr("core.details"), detailText);
583
584 // Set up Type callback if keyboard is available
585 auto* kb = core::getKeyboard();
586 if (kb && kb->isConnected() && s_passwordToType[0]) {
587 s_infoView.setYesNoCallbacks(onTypePassword, nullptr, nullptr);
588 s_infoView.setHint(ui::tr("mod_password.hint_type"));
589 } else {
590 s_infoView.setYesNoCallbacks(nullptr, nullptr, nullptr);
591 s_infoView.setHint(nullptr);
592 }
593
595}
596
600static void wizardFinish() {
601 bool ok = false;
602 if (s_wizard.editMode) {
604 } else {
606 }
607
608 if (ok) {
609 ui::showToastSuccess(ui::tr("core.saved"));
610 s_listView.preservePosition();
611 rebuildList();
613 } else {
614 ui::showToastError(ui::tr("core.failed"));
615 }
616}
617
625static void pushT9WizardStep(const char* title, const char* initialText,
626 uint16_t maxLen, ui::T9InputView::SaveCallback onSave) {
627 s_t9Input.init(title, initialText, maxLen);
628 s_t9Input.setOnSave(onSave);
630}
631
632static void onWizardTitle(const char* text);
633static void onWizardUsername(const char* text);
634static void onWizardPassword(const char* text);
635static void onWizardUrl(const char* text);
636static void onWizardTotp(const char* text);
637static void onWizardNotes(const char* text);
638
642static void wizardStart() {
643 memset(&s_wizard, 0, sizeof(s_wizard));
645 s_wizard.editMode = false;
646 s_wizard.editSlot = 0;
647
648 pushT9WizardStep(ui::tr("mod_password.field_title"), nullptr, PasswordStore::TITLE_LEN, onWizardTitle);
649}
650
655static void wizardEdit(uint16_t slot) {
656 PasswordEntry entry = {};
657 if (!PasswordStore::instance().readEntry(slot, &entry)) {
658 ui::showToastError(ui::tr("core.failed"));
659 return;
660 }
661
662 memset(&s_wizard, 0, sizeof(s_wizard));
663 s_wizard.entry = entry;
664 s_wizard.editMode = true;
665 s_wizard.editSlot = slot;
666
667 pushT9WizardStep(ui::tr("mod_password.field_title"), s_wizard.entry.title, PasswordStore::TITLE_LEN, onWizardTitle);
668}
669
674static void onWizardTitle(const char* text) {
675 strncpy(s_wizard.entry.title, text ? text : "", sizeof(s_wizard.entry.title) - 1);
676 s_wizard.entry.title[sizeof(s_wizard.entry.title) - 1] = '\0';
677 pushT9WizardStep(ui::tr("mod_password.username"), s_wizard.entry.username, PasswordStore::USERNAME_LEN, onWizardUsername);
678}
679
684static void onWizardUsername(const char* text) {
685 strncpy(s_wizard.entry.username, text ? text : "", sizeof(s_wizard.entry.username) - 1);
686 s_wizard.entry.username[sizeof(s_wizard.entry.username) - 1] = '\0';
687 s_t9Input.init(ui::tr("mod_password.password"), s_wizard.entry.password, PasswordStore::PASSWORD_LEN);
688 s_t9Input.setHint("x=Random Y=OK N=Back");
689 s_t9Input.setOnSave(onWizardPassword);
691}
692
698static void onWizardPassword(const char* text) {
699 if (text && text[0] == 'x' && text[1] == '\0') {
700 char generated[40] = {};
701 generateRandomPassword(generated, sizeof(generated));
702 strncpy(s_wizard.entry.password, generated, sizeof(s_wizard.entry.password) - 1);
703 } else {
704 strncpy(s_wizard.entry.password, text ? text : "", sizeof(s_wizard.entry.password) - 1);
705 }
706 s_wizard.entry.password[sizeof(s_wizard.entry.password) - 1] = '\0';
707 pushT9WizardStep(ui::tr("mod_password.url"), s_wizard.entry.url, PasswordStore::URL_LEN, onWizardUrl);
708}
709
714static void onWizardUrl(const char* text) {
715 strncpy(s_wizard.entry.url, text ? text : "", sizeof(s_wizard.entry.url) - 1);
716 s_wizard.entry.url[sizeof(s_wizard.entry.url) - 1] = '\0';
717
718 char totpBuf[8] = {};
719 if (s_wizard.entry.totpSlot != PasswordStore::TOTP_SLOT_NONE) {
720 snprintf(totpBuf, sizeof(totpBuf), "%u", s_wizard.entry.totpSlot);
721 }
722 pushT9WizardStep(ui::tr("mod_password.totp_slot"), totpBuf, 3, onWizardTotp);
723}
724
729static void onWizardTotp(const char* text) {
730 if (!text || !text[0]) {
732 } else {
733 int value = atoi(text);
734 if (value < 0 || value > 254) {
735 ui::showToastError(ui::tr("mod_password.invalid_input"));
736 pushT9WizardStep(ui::tr("mod_password.totp_slot"), text, 3, onWizardTotp);
737 return;
738 }
739 s_wizard.entry.totpSlot = static_cast<uint8_t>(value);
740 }
741
742 pushT9WizardStep(ui::tr("mod_password.notes"), s_wizard.entry.notes, NOTES_INPUT_MAX, onWizardNotes);
743}
744
749static void onWizardNotes(const char* text) {
750 strncpy(s_wizard.entry.notes, text ? text : "", sizeof(s_wizard.entry.notes) - 1);
751 s_wizard.entry.notes[sizeof(s_wizard.entry.notes) - 1] = '\0';
752 wizardFinish();
753}
754
758static void onMenuView() {
760}
761
765static void onMenuEdit() {
767}
768
773static void onMenuDeleteConfirm(void* userData) {
774 uint16_t slot = *static_cast<uint16_t*>(userData);
775 bool ok = PasswordStore::instance().deleteEntry(slot);
776 if (ok) {
777 ui::showToastSuccess(ui::tr("core.deleted"));
778 s_listView.preservePosition();
779 rebuildList();
781 } else {
782 ui::showToastError(ui::tr("core.failed"));
783 }
784}
785
789static void onMenuDelete() {
790 static uint16_t slot = 0;
791 slot = s_activeSlot;
792 ui::showConfirm(ui::tr("mod_password.confirm_delete"), onMenuDeleteConfirm, nullptr,
794}
795
801static void onListMenu(uint16_t index, void* userData) {
802 (void)userData;
803 if (index == 0) {
804 static ui::ContextMenuItem items[] = {
805 {ui::tr("mod_password.new_entry"), []() { wizardStart(); }}
806 };
807 ui::showContextMenu(ui::tr("mod_password.actions"), items, 1);
808 return;
809 }
810 if (index - 1 >= s_entryCount) return;
811 s_activeSlot = s_entries[index - 1].slot;
812
813 static ui::ContextMenuItem items[] = {
814 {ui::tr("mod_password.view"), onMenuView},
815 {ui::tr("mod_password.edit"), onMenuEdit},
816 {ui::tr("core.delete"), onMenuDelete}
817 };
818 ui::showContextMenu(ui::tr("mod_password.actions"), items, 3);
819}
820
826static void onListSelect(uint16_t index, void* userData) {
827 (void)userData;
828 if (index == 0) {
829 wizardStart();
830 return;
831 }
832 if (index - 1 >= s_entryCount) return;
833 s_activeSlot = s_entries[index - 1].slot;
835}
836
841PasswordModule& PasswordModule::instance() {
842 static PasswordModule inst;
843 return inst;
844}
845
851 LOG_I(TAG, "Initializing Password module");
854
856 if (slotRange_.hasRmem) {
859 } else {
860 core::ModuleRegistry::instance().reportModuleError(getName(), "Password slot range missing");
862 return false;
863 }
865 return true;
866}
867
873 ModuleBase::stop();
874}
875
881 slotRange_ = range;
882}
883
890 req.mapName = getName();
891 req.minRmemSlots = 1;
892 return req;
893}
894
901uint8_t PasswordModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
902 if (!items || maxItems == 0) return 0;
903
904 items[0] = {ui::tr("mod_password.title"), 55, []() -> ui::IView* {
905 if (!s_viewsInitialized) {
906 s_listView.setOnSelect(onListSelect);
907 s_listView.setOnMenu(onListMenu);
908 s_viewsInitialized = true;
909 }
910 if (!PasswordStore::instance().hasSlotRange()) {
911 ui::showToastError(ui::tr("mod_password.slot_error"));
912 return nullptr;
913 }
914 rebuildList();
915 return &s_listView;
916 }, nullptr, getName(), core::MenuLocation::MAIN_MENU, nullptr};
917
918 return 1;
919}
920
922static constexpr int kSchemaVer = 1;
923
935 if (!out) return false;
936
937 auto& store = PasswordStore::instance();
938 if (!store.hasSlotRange()) return false;
939
940 cJSON_AddNumberToObject(out, "schema_ver", kSchemaVer);
941 cJSON* entries = cJSON_AddArrayToObject(out, "entries");
942 if (!entries) return false;
943
944 struct ExportCtx {
945 cJSON* arr;
946 uint16_t count;
947 } ctx = { entries, 0 };
948
949 auto cb = [](uint16_t slot, const cdc::core::TropicStorage::CacheEntry&, void* user) {
950 auto* c = static_cast<ExportCtx*>(user);
951 auto& store = PasswordStore::instance();
952
953 uint16_t logical = 0;
954 if (!store.toLogicalSlot(slot, &logical)) return;
955
956 PasswordEntry entry = {};
957 if (!store.readEntry(logical, &entry)) return;
958
959 cJSON* obj = cJSON_CreateObject();
960 if (!obj) return;
961
962 cJSON_AddStringToObject(obj, "title", entry.title);
963 cJSON_AddStringToObject(obj, "username", entry.username);
964 cJSON_AddStringToObject(obj, "password", entry.password);
965 cJSON_AddStringToObject(obj, "url", entry.url);
966 cJSON_AddStringToObject(obj, "notes", entry.notes);
967 cJSON_AddNumberToObject(obj, "totp_slot", entry.totpSlot);
968
969 cJSON_AddItemToArray(c->arr, obj);
970 c->count++;
971 };
972
974 store.moduleId(),
975 store.rmemStart(),
976 store.rmemEnd(),
977 cb, &ctx);
978
979 return ctx.count > 0;
980}
981
994static bool importPasswordEntry(const cJSON* je, void* user) {
995 (void)user;
996 if (!cJSON_IsObject(je)) return false;
997
998 const cJSON* jTitle = cJSON_GetObjectItemCaseSensitive(je, "title");
999 const cJSON* jUsername = cJSON_GetObjectItemCaseSensitive(je, "username");
1000 const cJSON* jPassword = cJSON_GetObjectItemCaseSensitive(je, "password");
1001 const cJSON* jUrl = cJSON_GetObjectItemCaseSensitive(je, "url");
1002 const cJSON* jNotes = cJSON_GetObjectItemCaseSensitive(je, "notes");
1003 const cJSON* jTotpSlot = cJSON_GetObjectItemCaseSensitive(je, "totp_slot");
1004
1005 if (!cJSON_IsString(jTitle) || !jTitle->valuestring || jTitle->valuestring[0] == '\0') {
1006 LOG_W(TAG, "Password import: skipping entry with no title");
1007 return false;
1008 }
1009
1010 PasswordEntry entry = {};
1011 auto copyField = [](char* dst, size_t dstSize, const cJSON* j) {
1012 if (cJSON_IsString(j) && j->valuestring) {
1013 strncpy(dst, j->valuestring, dstSize - 1);
1014 dst[dstSize - 1] = '\0';
1015 }
1016 };
1017 copyField(entry.title, sizeof(entry.title), jTitle);
1018 copyField(entry.username, sizeof(entry.username), jUsername);
1019 copyField(entry.password, sizeof(entry.password), jPassword);
1020 copyField(entry.url, sizeof(entry.url), jUrl);
1021 copyField(entry.notes, sizeof(entry.notes), jNotes);
1022 entry.totpSlot = cJSON_IsNumber(jTotpSlot)
1023 ? static_cast<uint8_t>(jTotpSlot->valuedouble)
1025
1026 auto& store = PasswordStore::instance();
1027 uint16_t existingSlot = 0;
1028 if (store.findByTitle(entry.title, &existingSlot)) {
1029 return store.updateEntry(existingSlot, entry);
1030 }
1031 return store.addEntry(entry);
1032}
1033
1044 if (!in) return {};
1045 if (!PasswordStore::instance().hasSlotRange()) return {};
1046
1047 const cJSON* schemaVer = cJSON_GetObjectItemCaseSensitive(in, "schema_ver");
1048 if (cJSON_IsNumber(schemaVer) && static_cast<int>(schemaVer->valuedouble) != kSchemaVer) {
1049 LOG_W(TAG, "Password backup schema_ver %d != expected %d, skipping",
1050 static_cast<int>(schemaVer->valuedouble), kSchemaVer);
1051 return {};
1052 }
1053
1054 const cJSON* entries = cJSON_GetObjectItemCaseSensitive(in, "entries");
1055 return cdc::ui::importJsonArray(entries, importPasswordEntry, nullptr);
1056}
1057
1058} // namespace cdc::mod_password
1059
1063extern "C" void mod_password_register() {
1065 auto& module = cdc::mod_password::PasswordModule::instance();
1066 module.init();
1067 });
1068}
static const char * TAG
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
void mod_password_register()
Registers password module initializer in 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
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.
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 setSlotRange(const core::IModule::SlotRange &range) override
Stores assigned Tropic slot range for this module.
uint8_t getMenuItems(core::ModuleMenuItem *items, uint8_t maxItems) override
Provides main-menu entry for password module UI.
bool init() override
Initializes module resources, translations, commands, and slot mapping.
bool exportBackup(cJSON *out) override
Exports all vault entries into the module's backup section.
static PasswordModule & instance()
Returns singleton password module instance.
core::IModule::BackupResult importBackup(const cJSON *in) override
Restores vault entries from the module's backup section.
core::IModule::SlotRequest getSlotRequest() const override
Declares slot requirements for password storage.
void stop() override
Stops the password module and frees list resources.
static constexpr uint8_t PASSWORD_LEN
bool updateEntry(uint16_t slot, const PasswordEntry &entry)
Updates existing password entry.
static constexpr uint8_t TITLE_LEN
bool addEntry(const PasswordEntry &entry)
Adds a new password entry into first free slot.
static constexpr size_t NOTES_LEN
static constexpr uint8_t TOTP_SLOT_NONE
static constexpr uint8_t USERNAME_LEN
static PasswordStore & instance()
Returns singleton password store instance.
bool listEntriesSorted(EntryIndex *entries, uint16_t maxEntries, uint16_t *countOut) const
Lists entries sorted alphabetically by title.
bool deleteEntry(uint16_t slot)
Deletes entry at logical slot index.
static constexpr uint8_t URL_LEN
void setSlotRange(const cdc::core::IModule::SlotRange &range)
Configures logical-to-physical slot mapping for password entries.
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 constexpr uint16_t MAX_TEXT_LEN
Definition InfoView.h:22
void(*)(const char *text) SaveCallback
Definition T9InputView.h:29
static constexpr uint16_t MAX_TEXT_LEN
Definition T9InputView.h:22
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)
const char * skipSpaces(const char *s)
Advances over leading ASCII whitespace in a C string.
Definition StringUtils.h:13
IKeyboardProvider * getKeyboard()
void unescapeSpaces(char *s)
Replaces every \ escape sequence with a single space character in-place.
Definition StringUtils.h:61
const char * nextToken(const char *s, char *out, size_t outSize)
Extracts one whitespace-delimited token from a string.
Definition StringUtils.h:31
ISecureElement * getSecureElementInstance()
Returns singleton secure-element stub instance.
static void onMenuView()
Opens details view for currently active entry.
static void cmd_password_add(const char *args)
static const cdc::serial::SubCommand kPasswordSubs[]
constexpr ui::I18nEntry kStrings[]
static void showDetails(uint16_t slot)
Shows full entry details in the info view for a slot.
static bool isValidSlot(uint16_t slot)
Validates that a slot number is within the configured password range.
static void onWizardPassword(const char *text)
Saves password field; an "x" input generates a random 16-char password via the shared generator....
static constexpr int kSchemaVer
Schema version written to and expected from the password backup section.
const char * skipSpaces(const char *s)
Advances over leading ASCII whitespace in a C string.
Definition StringUtils.h:13
static void cmd_password_list(const char *args)
Serial command handler listing all password entries.
static void cmd_password_get(const char *args)
Serial command handler printing one password entry by index.
static void onWizardUrl(const char *text)
Saves URL field and advances to optional TOTP slot step.
static PasswordStore::EntryIndex * s_entries
static void wizardFinish()
Persists wizard add/edit changes and returns to list view.
static bool s_viewsInitialized
static void registerStrings()
static void rebuildList()
Rebuilds password list items from sorted store entries.
static void onWizardTotp(const char *text)
Validates and saves optional TOTP slot, then advances to notes step.
static uint16_t s_entryCount
static void wizardEdit(uint16_t slot)
Starts edit-entry wizard prefilled with existing slot data.
static uint16_t s_capacity
static void onListSelect(uint16_t index, void *userData)
Handles direct selection from list view (view existing or add new).
static void cmd_password_edit(const char *args)
Serial command handler editing one field of a password entry. Usage: PASSWORD_EDIT <index> <field> <n...
static void onTypePassword(void *userData)
Types currently selected password through attached keyboard provider.
const char * nextToken(const char *s, char *out, size_t outSize)
Extracts one whitespace-delimited token from a string.
Definition StringUtils.h:31
static bool importPasswordEntry(const cJSON *je, void *user)
Maps and upserts one vault entry from its JSON representation.
static bool isPlaceholder(const char *s)
Serial command handler adding one password entry.
static void cmd_password(const char *args)
static constexpr uint16_t NOTES_INPUT_MAX
static void cmd_password_del(const char *args)
Serial command handler deleting one password entry by index.
static void pushT9WizardStep(const char *title, const char *initialText, uint16_t maxLen, ui::T9InputView::SaveCallback onSave)
Pushes a configured T9 input step for wizard flow.
static constexpr const char * CMD_MODULE
Serial command handlers for password module.
static void onWizardTitle(const char *text)
Saves title field and advances to username step.
static void registerCommands()
Registers serial commands exposed by the password module.
static void generateRandomPassword(char *out, size_t outSize)
Generates a 16-character random password from charset a-zA-Z0-9$!%=.
static bool ensureListBuffers()
Ensures list and entry buffers are allocated for current store capacity.
static ui::ListView s_listView
Password module UI state and reusable view instances.
static bool s_commandsRegistered
static void onListMenu(uint16_t index, void *userData)
Opens contextual action menu for selected list entry.
static ui::ListItem * s_listItems
static void wizardStart()
Starts add-entry wizard with empty fields.
static void freeListBuffers()
Releases dynamic buffers used by the password list view.
static void onMenuDelete()
Opens delete confirmation dialog for currently active entry.
static ui::T9InputView s_t9Input
static void onMenuDeleteConfirm(void *userData)
Confirmation callback deleting selected entry slot.
static void onWizardNotes(const char *text)
Saves notes field and completes wizard persistence.
static ui::InfoView s_infoView
static WizardState s_wizard
static void onWizardUsername(const char *text)
Saves username field and advances to password step.
static void onMenuEdit()
Opens edit wizard for currently active entry.
static uint16_t s_activeSlot
static char s_passwordToType[PasswordStore::PASSWORD_LEN+1]
Shared output buffer used for keyboard typing callback payload.
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
cdc::core::IModule::BackupResult importJsonArray(const cJSON *array, BackupEntryHandler handler, void *user)
Iterates a JSON backup array best-effort and tallies the outcome.
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.
ContextMenuView * showContextMenu(const char *title, const ContextMenuItem *items, uint8_t count)
Shows the shared context menu instance as modal.
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
char password[PASSWORD_PASSWORD_LEN+1]
char url[PASSWORD_URL_LEN+1]
char title[PASSWORD_TITLE_LEN+1]
char notes[PASSWORD_NOTES_LEN+1]
char username[PASSWORD_USERNAME_LEN+1]
Single English translation entry.
Definition I18n.h:44