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.delete", "Delete"},
47 {"mod_password.actions", "Actions"},
48 {"mod_password.saved", "Saved"},
49 {"mod_password.deleted", "Deleted"},
50 {"mod_password.invalid_input", "Invalid input"},
51 {"mod_password.slot_error", "Slot map error"},
52 {"mod_password.details", "Details"},
53 {"mod_password.hint_list", "[Y] View [3] Menu [N] Back"},
54 {"mod_password.confirm_delete", "Delete entry?"},
55 {"mod_password.hint_type", "[Y] Type [2/8] Scroll [N] Back"},
56 {"mod_password.no_keyboard", "No keyboard connected"},
57};
58
62
64
65static constexpr const char* CMD_MODULE = "password";
66static bool s_commandsRegistered = false;
67
70
76static bool isValidSlot(uint16_t slot) {
77 auto& store = PasswordStore::instance();
78 return store.hasSlotRange() && slot < store.capacity();
79}
80
85static void cmd_password_list(const char* args) {
86 (void)args;
87 auto& store = PasswordStore::instance();
88 if (!store.hasSlotRange()) {
89 cdc::serial::Console::printf("ERROR: slot map not configured\r\n");
90 return;
91 }
92 uint16_t cap = store.capacity();
93 if (cap == 0) {
94 cdc::serial::Console::printf("(no entries)\r\n");
95 return;
96 }
97 auto list = std::unique_ptr<PasswordStore::EntryIndex[]>(new (std::nothrow) PasswordStore::EntryIndex[cap]);
98 if (!list) {
99 cdc::serial::Console::printf("ERROR: out of memory\r\n");
100 return;
101 }
102 uint16_t count = 0;
103 if (!store.listEntriesSorted(list.get(), cap, &count)) {
104 cdc::serial::Console::printf("ERROR: list failed\r\n");
105 return;
106 }
107 if (count == 0) {
108 cdc::serial::Console::printf("(no entries)\r\n");
109 return;
110 }
111 for (uint16_t i = 0; i < count; i++) {
112 cdc::serial::Console::printf("slot %u: %s\r\n", list[i].slot, list[i].title);
113 }
114}
115
120static void cmd_password_get(const char* args) {
121 char slotBuf[8] = {};
122 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
123 if (!p || !slotBuf[0]) {
124 cdc::serial::Console::printf("Usage: PASSWORD GET <slot>\r\n");
125 return;
126 }
127 uint16_t slot = static_cast<uint16_t>(atoi(slotBuf));
128 if (!isValidSlot(slot)) {
129 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
130 return;
131 }
132 PasswordEntry entry = {};
133 if (!PasswordStore::instance().readEntry(slot, &entry)) {
134 cdc::serial::Console::printf("ERROR: empty slot or read failed\r\n");
135 return;
136 }
137 cdc::serial::Console::printf("Title: %s\r\n", entry.title);
138 cdc::serial::Console::printf("Username: %s\r\n", entry.username);
139 cdc::serial::Console::printf("Password: %s\r\n", entry.password);
140 cdc::serial::Console::printf("URL: %s\r\n", entry.url);
142 cdc::serial::Console::printf("TOTP Slot: none\r\n");
143 } else {
144 cdc::serial::Console::printf("TOTP Slot: %u\r\n", entry.totpSlot);
145 }
146 cdc::serial::Console::printf("Notes: %s\r\n", entry.notes);
147}
148
153static bool isPlaceholder(const char* s) {
154 return s && s[0] && s[1] == '\0' && (s[0] == 'x' || s[0] == 'X' || s[0] == '-');
155}
156
161static void generateRandomPassword(char* out, size_t outSize) {
162 static const char charset[] =
163 "abcdefghijklmnopqrstuvwxyz"
164 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
165 "0123456789"
166 "$!%=";
167 constexpr size_t charsetLen = sizeof(charset) - 1;
168 constexpr uint8_t charCount = 16;
169 if (outSize < charCount + 1) return;
170
171 uint8_t rand[charCount] = {};
173 bool gotRand = se && se->getRandom(rand, sizeof(rand));
174 if (!gotRand) {
175 for (uint8_t i = 0; i < charCount; i++) {
176 rand[i] = static_cast<uint8_t>(esp_random() & 0xFF);
177 }
178 }
179
180 for (uint8_t i = 0; i < charCount; i++) {
181 out[i] = charset[rand[i] % charsetLen];
182 }
183 out[charCount] = '\0';
184}
185
186static void cmd_password_add(const char* args) {
187 PasswordEntry entry = {};
189
190 char slotBuf[8] = {};
191 char title[PasswordStore::TITLE_LEN + 1] = {};
192 char username[PasswordStore::USERNAME_LEN + 1] = {};
193 char password[PasswordStore::PASSWORD_LEN + 1] = {};
194 char url[PasswordStore::URL_LEN + 1] = {};
195 char totpBuf[8] = {};
196
197 static constexpr const char* USAGE =
198 "Usage: PASSWORD ADD <slot|x> <title> <username|x> <password|x> <url|x> <totp|-> [notes]\r\n"
199 " <slot>: target RMEM slot, or 'x' for next free. Overwrites if occupied.\r\n"
200 " <password>: value, or 'x' to generate a random 16-char password.\r\n"
201 " <totp>: linked TOTP slot number, or '-' for no link.\r\n"
202 " Use 'x' for username/url to skip them. Use '\\\\ ' for spaces inside fields.\r\n";
203
204 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
205 if (!p || !slotBuf[0]) {
206 cdc::serial::Console::printf("%s", USAGE);
207 return;
208 }
209 p = nextToken(p, title, sizeof(title));
210 p = nextToken(p, username, sizeof(username));
211 p = nextToken(p, password, sizeof(password));
212 p = nextToken(p, url, sizeof(url));
213 p = nextToken(p, totpBuf, sizeof(totpBuf));
214 if (!title[0] || !username[0] || !password[0] || !url[0] || !totpBuf[0]) {
215 cdc::serial::Console::printf("%s", USAGE);
216 return;
217 }
218
219 const char* notes = skipSpaces(p);
220
221 auto& store = PasswordStore::instance();
222
223 uint16_t slot = 0;
224 if (isPlaceholder(slotBuf)) {
225 if (!store.findFreeLogicalSlot(&slot)) {
226 cdc::serial::Console::printf("ERROR: no free slots\r\n");
227 return;
228 }
229 } else {
230 slot = static_cast<uint16_t>(atoi(slotBuf));
231 if (!isValidSlot(slot)) {
232 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
233 return;
234 }
235 }
236
237 strncpy(entry.title, title, sizeof(entry.title) - 1);
238 if (!isPlaceholder(username)) {
239 strncpy(entry.username, username, sizeof(entry.username) - 1);
240 }
241
242 char generatedPassword[40] = {};
243 if (password[0] == 'x' && password[1] == '\0') {
244 generateRandomPassword(generatedPassword, sizeof(generatedPassword));
245 strncpy(entry.password, generatedPassword, sizeof(entry.password) - 1);
246 } else if (!isPlaceholder(password)) {
247 strncpy(entry.password, password, sizeof(entry.password) - 1);
248 }
249
250 if (!isPlaceholder(url)) {
251 strncpy(entry.url, url, sizeof(entry.url) - 1);
252 }
253
254 if (totpBuf[0] != '-' || totpBuf[1] != '\0') {
255 int totp = atoi(totpBuf);
256 if (totp < 0 || totp > 254) {
257 cdc::serial::Console::printf("ERROR: totp slot out of range (0-254 or '-')\r\n");
258 return;
259 }
260 entry.totpSlot = static_cast<uint8_t>(totp);
261 }
262
263 if (notes && notes[0]) {
264 strncpy(entry.notes, notes, sizeof(entry.notes) - 1);
266 }
267
268 bool ok = store.updateEntry(slot, entry);
269 if (ok) {
270 if (generatedPassword[0]) {
271 cdc::serial::Console::printf("Generated password: %s\r\n", generatedPassword);
272 }
273 cdc::serial::Console::printf("OK (slot %u)\r\n", slot);
274 } else {
275 cdc::serial::Console::printf("ERROR\r\n");
276 }
277}
278
284static void cmd_password_edit(const char* args) {
285 char slotBuf[8] = {};
286 char field[16] = {};
287 const char* usage =
288 "Usage: PASSWORD EDIT <slot> <title|username|password|url|totp|notes> <value>\r\n"
289 " Use '\\\\ ' for spaces inside values.\r\n";
290
291 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
292 if (!p || !slotBuf[0]) {
293 cdc::serial::Console::printf("%s", usage);
294 return;
295 }
296 p = nextToken(p, field, sizeof(field));
297 if (!field[0]) {
298 cdc::serial::Console::printf("%s", usage);
299 return;
300 }
301
302 uint16_t slot = static_cast<uint16_t>(atoi(slotBuf));
303 if (!isValidSlot(slot)) {
304 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
305 return;
306 }
307
308 PasswordEntry entry = {};
309 if (!PasswordStore::instance().readEntry(slot, &entry)) {
310 cdc::serial::Console::printf("ERROR: empty slot or read failed\r\n");
311 return;
312 }
313
314 const char* value = skipSpaces(p);
315 if (!value || !value[0]) {
316 cdc::serial::Console::printf("ERROR: empty value\r\n");
317 return;
318 }
319
320 if (strcasecmp(field, "title") == 0) {
321 if (strlen(value) > PasswordStore::TITLE_LEN) {
322 cdc::serial::Console::printf("ERROR: title too long (max %u)\r\n", PasswordStore::TITLE_LEN);
323 return;
324 }
325 memset(entry.title, 0, sizeof(entry.title));
326 strncpy(entry.title, value, sizeof(entry.title) - 1);
328 } else if (strcasecmp(field, "username") == 0) {
329 if (strlen(value) > PasswordStore::USERNAME_LEN) {
330 cdc::serial::Console::printf("ERROR: username too long (max %u)\r\n", PasswordStore::USERNAME_LEN);
331 return;
332 }
333 memset(entry.username, 0, sizeof(entry.username));
334 strncpy(entry.username, value, sizeof(entry.username) - 1);
336 } else if (strcasecmp(field, "password") == 0) {
337 if (strlen(value) > PasswordStore::PASSWORD_LEN) {
338 cdc::serial::Console::printf("ERROR: password too long (max %u)\r\n", PasswordStore::PASSWORD_LEN);
339 return;
340 }
341 memset(entry.password, 0, sizeof(entry.password));
342 strncpy(entry.password, value, sizeof(entry.password) - 1);
344 } else if (strcasecmp(field, "url") == 0) {
345 if (strlen(value) > PasswordStore::URL_LEN) {
346 cdc::serial::Console::printf("ERROR: url too long (max %u)\r\n", PasswordStore::URL_LEN);
347 return;
348 }
349 memset(entry.url, 0, sizeof(entry.url));
350 strncpy(entry.url, value, sizeof(entry.url) - 1);
352 } else if (strcasecmp(field, "totp") == 0) {
353 int totp = atoi(value);
354 if (totp < 0 || totp > 254) {
355 cdc::serial::Console::printf("ERROR: totp slot out of range (0-254)\r\n");
356 return;
357 }
358 entry.totpSlot = static_cast<uint8_t>(totp);
359 } else if (strcasecmp(field, "notes") == 0) {
360 if (strlen(value) > PasswordStore::NOTES_LEN) {
361 cdc::serial::Console::printf("ERROR: notes too long (max %u)\r\n", static_cast<unsigned>(PasswordStore::NOTES_LEN));
362 return;
363 }
364 memset(entry.notes, 0, sizeof(entry.notes));
365 strncpy(entry.notes, value, sizeof(entry.notes) - 1);
367 } else {
368 cdc::serial::Console::printf("%s", usage);
369 return;
370 }
371
372 bool ok = PasswordStore::instance().updateEntry(slot, entry);
373 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
374}
375
380static void cmd_password_del(const char* args) {
381 char slotBuf[8] = {};
382 const char* p = nextToken(args, slotBuf, sizeof(slotBuf));
383 if (!p || !slotBuf[0]) {
384 cdc::serial::Console::printf("Usage: PASSWORD DEL <slot>\r\n");
385 return;
386 }
387 uint16_t slot = static_cast<uint16_t>(atoi(slotBuf));
388 if (!isValidSlot(slot)) {
389 cdc::serial::Console::printf("ERROR: slot out of range\r\n");
390 return;
391 }
392 bool ok = PasswordStore::instance().deleteEntry(slot);
393 cdc::serial::Console::printf(ok ? "OK\r\n" : "ERROR\r\n");
394}
395
397 {"LIST", "", "List password entries (sorted by title)", cmd_password_list},
398 {"GET", "<slot>", "Show one entry by slot", cmd_password_get},
399 {"ADD", "<slot|x> <title> <user|x> <pw|x> <url|x> <totp|-> [notes]", "Add entry; 'x' for fields skips them", cmd_password_add},
400 {"EDIT", "<slot> <field> <value>", "Edit one field of an existing entry", cmd_password_edit},
401 {"DEL", "<slot>", "Delete entry by slot", cmd_password_del},
402 {nullptr, nullptr, nullptr, nullptr},
403};
404
405static void cmd_password(const char* args) {
407}
408
412static void registerCommands() {
413 if (s_commandsRegistered) return;
415
417 reg.registerCommand({"PASSWORD",
418 "Password vault: LIST/GET/ADD/EDIT/DEL",
420}
421
423
427static bool s_viewsInitialized = false;
428
429static ui::ListItem* s_listItems = nullptr;
431static uint16_t s_entryCount = 0;
432static uint16_t s_capacity = 0;
433
434static uint16_t s_activeSlot = 0;
435
441
442EXT_RAM_BSS_ATTR static WizardState s_wizard = {};
443
444static constexpr uint16_t NOTES_INPUT_MAX =
446 ? static_cast<uint16_t>(PasswordStore::NOTES_LEN)
447 : static_cast<uint16_t>(ui::T9InputView::MAX_TEXT_LEN);
448
452static void freeListBuffers() {
453 delete[] s_listItems;
454 delete[] s_entries;
455 s_listItems = nullptr;
456 s_entries = nullptr;
457 s_capacity = 0;
458 s_entryCount = 0;
459}
460
465static bool ensureListBuffers() {
466 uint16_t cap = PasswordStore::instance().capacity();
467 if (cap == 0) return false;
468 if (cap == s_capacity && s_listItems && s_entries) return true;
469
470 delete[] s_listItems;
471 delete[] s_entries;
472 s_listItems = nullptr;
473 s_entries = nullptr;
474 s_capacity = 0;
475
476 s_listItems = new (std::nothrow) ui::ListItem[cap + 1];
477 s_entries = new (std::nothrow) PasswordStore::EntryIndex[cap];
478 if (!s_listItems || !s_entries) {
479 delete[] s_listItems;
480 delete[] s_entries;
481 s_listItems = nullptr;
482 s_entries = nullptr;
483 s_capacity = 0;
484 return false;
485 }
486 s_capacity = cap;
487 return true;
488}
489
493static void rebuildList() {
494 if (!PasswordStore::instance().hasSlotRange()) {
495 ui::showToastError(ui::tr("mod_password.slot_error"));
496 return;
497 }
498 if (!ensureListBuffers()) {
500 "Password list allocation failed");
501 return;
502 }
503 s_entryCount = 0;
504 s_listItems[0] = {ui::tr("mod_password.new_entry"), 0, false, nullptr};
505
506 uint16_t count = 0;
508 s_entryCount = count;
509
510 for (uint16_t i = 0; i < s_entryCount; i++) {
511 uint16_t idx = static_cast<uint16_t>(i + 1);
512 s_listItems[idx].label = s_entries[i].title;
513 s_listItems[idx].icon = 0;
514 s_listItems[idx].iconDisabled = false;
515 s_listItems[idx].userData = reinterpret_cast<void*>(static_cast<uintptr_t>(s_entries[i].slot));
516 }
517
518 s_listView.init(ui::tr("mod_password.title"), s_listItems, static_cast<uint16_t>(s_entryCount + 1));
519s_listView.setHint(ui::tr("mod_password.hint_list"));
520}
521
524
529static void onTypePassword(void* userData) {
530 (void)userData;
531 auto* kb = core::getKeyboard();
532 if (kb && kb->isConnected()) {
533 if (s_passwordToType[0]) {
534 kb->typeString(s_passwordToType);
535 ui::showToastSuccess("Typed");
536 }
537 } else {
538 ui::showToastError(ui::tr("mod_password.no_keyboard"));
539 }
540}
541
546static void showDetails(uint16_t slot) {
547 PasswordEntry entry = {};
548 if (!PasswordStore::instance().readEntry(slot, &entry)) {
549 ui::showToastError(ui::tr("core.failed"));
550 return;
551 }
552
553 // Store password for type callback
554 strncpy(s_passwordToType, entry.password, sizeof(s_passwordToType) - 1);
555 s_passwordToType[sizeof(s_passwordToType) - 1] = '\0';
556
557 static EXT_RAM_BSS_ATTR char detailText[ui::InfoView::MAX_TEXT_LEN];
558 char totpBuf[16] = {};
559 const char* emptyText = ui::tr("core.empty");
560 char emptyWrapped[16] = {};
561 snprintf(emptyWrapped, sizeof(emptyWrapped), "(%s)", emptyText);
562 const char* totpText = emptyWrapped;
564 snprintf(totpBuf, sizeof(totpBuf), "%u", entry.totpSlot);
565 totpText = totpBuf;
566 }
567 const char* usernameText = entry.username[0] ? entry.username : emptyWrapped;
568 const char* passwordText = entry.password[0] ? entry.password : emptyWrapped;
569 const char* urlText = entry.url[0] ? entry.url : emptyWrapped;
570 const char* notesText = entry.notes[0] ? entry.notes : emptyWrapped;
571
572 snprintf(detailText, sizeof(detailText),
573 "Title: %s\n"
574 "Username: %s\n"
575 "Password: %s\n"
576 "URL: %s\n"
577 "TOTP Slot: %s\n"
578 "Notes: %s",
579 entry.title,
580 usernameText,
581 passwordText,
582 urlText,
583 totpText,
584 notesText);
585
586 s_infoView.init(ui::tr("mod_password.details"), detailText);
587
588 // Set up Type callback if keyboard is available
589 auto* kb = core::getKeyboard();
590 if (kb && kb->isConnected() && s_passwordToType[0]) {
591 s_infoView.setYesNoCallbacks(onTypePassword, nullptr, nullptr);
592 s_infoView.setHint(ui::tr("mod_password.hint_type"));
593 } else {
594 s_infoView.setYesNoCallbacks(nullptr, nullptr, nullptr);
595 s_infoView.setHint(nullptr);
596 }
597
599}
600
604static void wizardFinish() {
605 bool ok = false;
606 if (s_wizard.editMode) {
608 } else {
610 }
611
612 if (ok) {
613 ui::showToastSuccess(ui::tr("mod_password.saved"));
614 s_listView.preservePosition();
615 rebuildList();
617 } else {
618 ui::showToastError(ui::tr("core.failed"));
619 }
620}
621
629static void pushT9WizardStep(const char* title, const char* initialText,
630 uint16_t maxLen, ui::T9InputView::SaveCallback onSave) {
631 s_t9Input.init(title, initialText, maxLen);
632 s_t9Input.setOnSave(onSave);
634}
635
636static void onWizardTitle(const char* text);
637static void onWizardUsername(const char* text);
638static void onWizardPassword(const char* text);
639static void onWizardUrl(const char* text);
640static void onWizardTotp(const char* text);
641static void onWizardNotes(const char* text);
642
646static void wizardStart() {
647 memset(&s_wizard, 0, sizeof(s_wizard));
649 s_wizard.editMode = false;
650 s_wizard.editSlot = 0;
651
652 pushT9WizardStep(ui::tr("mod_password.field_title"), nullptr, PasswordStore::TITLE_LEN, onWizardTitle);
653}
654
659static void wizardEdit(uint16_t slot) {
660 PasswordEntry entry = {};
661 if (!PasswordStore::instance().readEntry(slot, &entry)) {
662 ui::showToastError(ui::tr("core.failed"));
663 return;
664 }
665
666 memset(&s_wizard, 0, sizeof(s_wizard));
667 s_wizard.entry = entry;
668 s_wizard.editMode = true;
669 s_wizard.editSlot = slot;
670
671 pushT9WizardStep(ui::tr("mod_password.field_title"), s_wizard.entry.title, PasswordStore::TITLE_LEN, onWizardTitle);
672}
673
678static void onWizardTitle(const char* text) {
679 strncpy(s_wizard.entry.title, text ? text : "", sizeof(s_wizard.entry.title) - 1);
680 s_wizard.entry.title[sizeof(s_wizard.entry.title) - 1] = '\0';
681 pushT9WizardStep(ui::tr("mod_password.username"), s_wizard.entry.username, PasswordStore::USERNAME_LEN, onWizardUsername);
682}
683
688static void onWizardUsername(const char* text) {
689 strncpy(s_wizard.entry.username, text ? text : "", sizeof(s_wizard.entry.username) - 1);
690 s_wizard.entry.username[sizeof(s_wizard.entry.username) - 1] = '\0';
691 s_t9Input.init(ui::tr("mod_password.password"), s_wizard.entry.password, PasswordStore::PASSWORD_LEN);
692 s_t9Input.setHint("x=Random Y=OK N=Back");
693 s_t9Input.setOnSave(onWizardPassword);
695}
696
702static void onWizardPassword(const char* text) {
703 if (text && text[0] == 'x' && text[1] == '\0') {
704 char generated[40] = {};
705 generateRandomPassword(generated, sizeof(generated));
706 strncpy(s_wizard.entry.password, generated, sizeof(s_wizard.entry.password) - 1);
707 } else {
708 strncpy(s_wizard.entry.password, text ? text : "", sizeof(s_wizard.entry.password) - 1);
709 }
710 s_wizard.entry.password[sizeof(s_wizard.entry.password) - 1] = '\0';
711 pushT9WizardStep(ui::tr("mod_password.url"), s_wizard.entry.url, PasswordStore::URL_LEN, onWizardUrl);
712}
713
718static void onWizardUrl(const char* text) {
719 strncpy(s_wizard.entry.url, text ? text : "", sizeof(s_wizard.entry.url) - 1);
720 s_wizard.entry.url[sizeof(s_wizard.entry.url) - 1] = '\0';
721
722 char totpBuf[8] = {};
723 if (s_wizard.entry.totpSlot != PasswordStore::TOTP_SLOT_NONE) {
724 snprintf(totpBuf, sizeof(totpBuf), "%u", s_wizard.entry.totpSlot);
725 }
726 pushT9WizardStep(ui::tr("mod_password.totp_slot"), totpBuf, 3, onWizardTotp);
727}
728
733static void onWizardTotp(const char* text) {
734 if (!text || !text[0]) {
736 } else {
737 int value = atoi(text);
738 if (value < 0 || value > 254) {
739 ui::showToastError(ui::tr("mod_password.invalid_input"));
740 pushT9WizardStep(ui::tr("mod_password.totp_slot"), text, 3, onWizardTotp);
741 return;
742 }
743 s_wizard.entry.totpSlot = static_cast<uint8_t>(value);
744 }
745
746 pushT9WizardStep(ui::tr("mod_password.notes"), s_wizard.entry.notes, NOTES_INPUT_MAX, onWizardNotes);
747}
748
753static void onWizardNotes(const char* text) {
754 strncpy(s_wizard.entry.notes, text ? text : "", sizeof(s_wizard.entry.notes) - 1);
755 s_wizard.entry.notes[sizeof(s_wizard.entry.notes) - 1] = '\0';
756 wizardFinish();
757}
758
762static void onMenuView() {
764}
765
769static void onMenuEdit() {
771}
772
777static void onMenuDeleteConfirm(void* userData) {
778 uint16_t slot = *static_cast<uint16_t*>(userData);
779 bool ok = PasswordStore::instance().deleteEntry(slot);
780 if (ok) {
781 ui::showToastSuccess(ui::tr("mod_password.deleted"));
782 s_listView.preservePosition();
783 rebuildList();
785 } else {
786 ui::showToastError(ui::tr("core.failed"));
787 }
788}
789
793static void onMenuDelete() {
794 static uint16_t slot = 0;
795 slot = s_activeSlot;
796 ui::showConfirm(ui::tr("mod_password.confirm_delete"), onMenuDeleteConfirm, nullptr,
798}
799
805static void onListMenu(uint16_t index, void* userData) {
806 (void)userData;
807 if (index == 0) {
808 static ui::ContextMenuItem items[] = {
809 {ui::tr("mod_password.new_entry"), []() { wizardStart(); }}
810 };
811 ui::showContextMenu(ui::tr("mod_password.actions"), items, 1);
812 return;
813 }
814 if (index - 1 >= s_entryCount) return;
815 s_activeSlot = s_entries[index - 1].slot;
816
817 static ui::ContextMenuItem items[] = {
818 {ui::tr("mod_password.view"), onMenuView},
819 {ui::tr("mod_password.edit"), onMenuEdit},
820 {ui::tr("mod_password.delete"), onMenuDelete}
821 };
822 ui::showContextMenu(ui::tr("mod_password.actions"), items, 3);
823}
824
830static void onListSelect(uint16_t index, void* userData) {
831 (void)userData;
832 if (index == 0) {
833 wizardStart();
834 return;
835 }
836 if (index - 1 >= s_entryCount) return;
837 s_activeSlot = s_entries[index - 1].slot;
839}
840
845PasswordModule& PasswordModule::instance() {
846 static PasswordModule inst;
847 return inst;
848}
849
855 LOG_I(TAG, "Initializing Password module");
858
860 if (slotRange_.hasRmem) {
863 } else {
864 core::ModuleRegistry::instance().reportModuleError(getName(), "Password slot range missing");
866 return false;
867 }
869 return true;
870}
871
877 ModuleBase::stop();
878}
879
885 slotRange_ = range;
886}
887
894 req.mapName = getName();
895 req.minRmemSlots = 1;
896 return req;
897}
898
905uint8_t PasswordModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
906 if (!items || maxItems == 0) return 0;
907
908 items[0] = {ui::tr("mod_password.title"), 55, []() -> ui::IView* {
909 if (!s_viewsInitialized) {
910 s_listView.setOnSelect(onListSelect);
911 s_listView.setOnMenu(onListMenu);
912 s_viewsInitialized = true;
913 }
914 if (!PasswordStore::instance().hasSlotRange()) {
915 ui::showToastError(ui::tr("mod_password.slot_error"));
916 return nullptr;
917 }
918 rebuildList();
919 return &s_listView;
920 }, nullptr, getName(), core::MenuLocation::MAIN_MENU, nullptr};
921
922 return 1;
923}
924
926static constexpr int kSchemaVer = 1;
927
939 if (!out) return false;
940
941 auto& store = PasswordStore::instance();
942 if (!store.hasSlotRange()) return false;
943
944 cJSON_AddNumberToObject(out, "schema_ver", kSchemaVer);
945 cJSON* entries = cJSON_AddArrayToObject(out, "entries");
946 if (!entries) return false;
947
948 struct ExportCtx {
949 cJSON* arr;
950 uint16_t count;
951 } ctx = { entries, 0 };
952
953 auto cb = [](uint16_t slot, const cdc::core::TropicStorage::CacheEntry&, void* user) {
954 auto* c = static_cast<ExportCtx*>(user);
955 auto& store = PasswordStore::instance();
956
957 uint16_t logical = 0;
958 if (!store.toLogicalSlot(slot, &logical)) return;
959
960 PasswordEntry entry = {};
961 if (!store.readEntry(logical, &entry)) return;
962
963 cJSON* obj = cJSON_CreateObject();
964 if (!obj) return;
965
966 cJSON_AddStringToObject(obj, "title", entry.title);
967 cJSON_AddStringToObject(obj, "username", entry.username);
968 cJSON_AddStringToObject(obj, "password", entry.password);
969 cJSON_AddStringToObject(obj, "url", entry.url);
970 cJSON_AddStringToObject(obj, "notes", entry.notes);
971 cJSON_AddNumberToObject(obj, "totp_slot", entry.totpSlot);
972
973 cJSON_AddItemToArray(c->arr, obj);
974 c->count++;
975 };
976
978 store.moduleId(),
979 store.rmemStart(),
980 store.rmemEnd(),
981 cb, &ctx);
982
983 return ctx.count > 0;
984}
985
998static bool importPasswordEntry(const cJSON* je, void* user) {
999 (void)user;
1000 if (!cJSON_IsObject(je)) return false;
1001
1002 const cJSON* jTitle = cJSON_GetObjectItemCaseSensitive(je, "title");
1003 const cJSON* jUsername = cJSON_GetObjectItemCaseSensitive(je, "username");
1004 const cJSON* jPassword = cJSON_GetObjectItemCaseSensitive(je, "password");
1005 const cJSON* jUrl = cJSON_GetObjectItemCaseSensitive(je, "url");
1006 const cJSON* jNotes = cJSON_GetObjectItemCaseSensitive(je, "notes");
1007 const cJSON* jTotpSlot = cJSON_GetObjectItemCaseSensitive(je, "totp_slot");
1008
1009 if (!cJSON_IsString(jTitle) || !jTitle->valuestring || jTitle->valuestring[0] == '\0') {
1010 LOG_W(TAG, "Password import: skipping entry with no title");
1011 return false;
1012 }
1013
1014 PasswordEntry entry = {};
1015 auto copyField = [](char* dst, size_t dstSize, const cJSON* j) {
1016 if (cJSON_IsString(j) && j->valuestring) {
1017 strncpy(dst, j->valuestring, dstSize - 1);
1018 dst[dstSize - 1] = '\0';
1019 }
1020 };
1021 copyField(entry.title, sizeof(entry.title), jTitle);
1022 copyField(entry.username, sizeof(entry.username), jUsername);
1023 copyField(entry.password, sizeof(entry.password), jPassword);
1024 copyField(entry.url, sizeof(entry.url), jUrl);
1025 copyField(entry.notes, sizeof(entry.notes), jNotes);
1026 entry.totpSlot = cJSON_IsNumber(jTotpSlot)
1027 ? static_cast<uint8_t>(jTotpSlot->valuedouble)
1029
1030 auto& store = PasswordStore::instance();
1031 uint16_t existingSlot = 0;
1032 if (store.findByTitle(entry.title, &existingSlot)) {
1033 return store.updateEntry(existingSlot, entry);
1034 }
1035 return store.addEntry(entry);
1036}
1037
1048 if (!in) return {};
1049 if (!PasswordStore::instance().hasSlotRange()) return {};
1050
1051 const cJSON* schemaVer = cJSON_GetObjectItemCaseSensitive(in, "schema_ver");
1052 if (cJSON_IsNumber(schemaVer) && static_cast<int>(schemaVer->valuedouble) != kSchemaVer) {
1053 LOG_W(TAG, "Password backup schema_ver %d != expected %d, skipping",
1054 static_cast<int>(schemaVer->valuedouble), kSchemaVer);
1055 return {};
1056 }
1057
1058 const cJSON* entries = cJSON_GetObjectItemCaseSensitive(in, "entries");
1059 return cdc::ui::importJsonArray(entries, importPasswordEntry, nullptr);
1060}
1061
1062} // namespace cdc::mod_password
1063
1067extern "C" void mod_password_register() {
1069 auto& module = cdc::mod_password::PasswordModule::instance();
1070 module.init();
1071 });
1072}
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: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 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:34
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:208
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