10#include "esp_random.h"
31static const char*
TAG =
"PASSWORD";
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"},
78 return store.hasSlotRange() && slot < store.capacity();
88 if (!store.hasSlotRange()) {
92 uint16_t cap = store.capacity();
103 if (!store.listEntriesSorted(list.get(), cap, &count)) {
111 for (uint16_t i = 0; i < count; i++) {
121 char slotBuf[8] = {};
122 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
123 if (!p || !slotBuf[0]) {
127 uint16_t slot =
static_cast<uint16_t
>(atoi(slotBuf));
154 return s && s[0] && s[1] ==
'\0' && (s[0] ==
'x' || s[0] ==
'X' || s[0] ==
'-');
162 static const char charset[] =
163 "abcdefghijklmnopqrstuvwxyz"
164 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
167 constexpr size_t charsetLen =
sizeof(charset) - 1;
168 constexpr uint8_t charCount = 16;
169 if (outSize < charCount + 1)
return;
171 uint8_t rand[charCount] = {};
173 bool gotRand = se && se->getRandom(rand,
sizeof(rand));
175 for (uint8_t i = 0; i < charCount; i++) {
176 rand[i] =
static_cast<uint8_t
>(esp_random() & 0xFF);
180 for (uint8_t i = 0; i < charCount; i++) {
181 out[i] = charset[rand[i] % charsetLen];
183 out[charCount] =
'\0';
190 char slotBuf[8] = {};
195 char totpBuf[8] = {};
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";
204 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
205 if (!p || !slotBuf[0]) {
210 p =
nextToken(p, username,
sizeof(username));
211 p =
nextToken(p, password,
sizeof(password));
213 p =
nextToken(p, totpBuf,
sizeof(totpBuf));
214 if (!title[0] || !username[0] || !password[0] || !url[0] || !totpBuf[0]) {
225 if (!store.findFreeLogicalSlot(&slot)) {
230 slot =
static_cast<uint16_t
>(atoi(slotBuf));
237 strncpy(entry.
title, title,
sizeof(entry.
title) - 1);
242 char generatedPassword[40] = {};
243 if (password[0] ==
'x' && password[1] ==
'\0') {
251 strncpy(entry.
url, url,
sizeof(entry.
url) - 1);
254 if (totpBuf[0] !=
'-' || totpBuf[1] !=
'\0') {
255 int totp = atoi(totpBuf);
256 if (totp < 0 || totp > 254) {
260 entry.
totpSlot =
static_cast<uint8_t
>(totp);
263 if (notes && notes[0]) {
264 strncpy(entry.
notes, notes,
sizeof(entry.
notes) - 1);
268 bool ok = store.updateEntry(slot, entry);
270 if (generatedPassword[0]) {
285 char slotBuf[8] = {};
288 "Usage: PASSWORD EDIT <slot> <title|username|password|url|totp|notes> <value>\r\n"
289 " Use '\\\\ ' for spaces inside values.\r\n";
291 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
292 if (!p || !slotBuf[0]) {
302 uint16_t slot =
static_cast<uint16_t
>(atoi(slotBuf));
315 if (!value || !value[0]) {
320 if (strcasecmp(field,
"title") == 0) {
326 strncpy(entry.
title, value,
sizeof(entry.
title) - 1);
328 }
else if (strcasecmp(field,
"username") == 0) {
336 }
else if (strcasecmp(field,
"password") == 0) {
344 }
else if (strcasecmp(field,
"url") == 0) {
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) {
358 entry.
totpSlot =
static_cast<uint8_t
>(totp);
359 }
else if (strcasecmp(field,
"notes") == 0) {
365 strncpy(entry.
notes, value,
sizeof(entry.
notes) - 1);
381 char slotBuf[8] = {};
382 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
383 if (!p || !slotBuf[0]) {
387 uint16_t slot =
static_cast<uint16_t
>(atoi(slotBuf));
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},
402 {
nullptr,
nullptr,
nullptr,
nullptr},
417 reg.registerCommand({
"PASSWORD",
418 "Password vault: LIST/GET/ADD/EDIT/DEL",
467 if (cap == 0)
return false;
500 "Password list allocation failed");
511 uint16_t idx =
static_cast<uint16_t
>(i + 1);
532 if (kb && kb->isConnected()) {
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);
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;
572 snprintf(detailText,
sizeof(detailText),
594 s_infoView.setYesNoCallbacks(
nullptr,
nullptr,
nullptr);
631 s_t9Input.init(title, initialText, maxLen);
679 strncpy(
s_wizard.entry.title, text ? text :
"",
sizeof(
s_wizard.entry.title) - 1);
689 strncpy(
s_wizard.entry.username, text ? text :
"",
sizeof(
s_wizard.entry.username) - 1);
692 s_t9Input.setHint(
"x=Random Y=OK N=Back");
703 if (text && text[0] ==
'x' && text[1] ==
'\0') {
704 char generated[40] = {};
706 strncpy(
s_wizard.entry.password, generated,
sizeof(
s_wizard.entry.password) - 1);
708 strncpy(
s_wizard.entry.password, text ? text :
"",
sizeof(
s_wizard.entry.password) - 1);
719 strncpy(
s_wizard.entry.url, text ? text :
"",
sizeof(
s_wizard.entry.url) - 1);
722 char totpBuf[8] = {};
724 snprintf(totpBuf,
sizeof(totpBuf),
"%u",
s_wizard.entry.totpSlot);
734 if (!text || !text[0]) {
737 int value = atoi(text);
738 if (value < 0 || value > 254) {
743 s_wizard.entry.totpSlot =
static_cast<uint8_t
>(value);
754 strncpy(
s_wizard.entry.notes, text ? text :
"",
sizeof(
s_wizard.entry.notes) - 1);
778 uint16_t slot = *
static_cast<uint16_t*
>(userData);
794 static uint16_t slot = 0;
846 static PasswordModule inst;
855 LOG_I(
TAG,
"Initializing Password module");
860 if (slotRange_.hasRmem) {
906 if (!items || maxItems == 0)
return 0;
939 if (!out)
return false;
942 if (!store.hasSlotRange())
return false;
944 cJSON_AddNumberToObject(out,
"schema_ver",
kSchemaVer);
945 cJSON* entries = cJSON_AddArrayToObject(out,
"entries");
946 if (!entries)
return false;
951 } ctx = { entries, 0 };
954 auto* c =
static_cast<ExportCtx*
>(user);
957 uint16_t logical = 0;
958 if (!store.toLogicalSlot(slot, &logical))
return;
961 if (!store.readEntry(logical, &entry))
return;
963 cJSON* obj = cJSON_CreateObject();
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);
973 cJSON_AddItemToArray(c->arr, obj);
983 return ctx.count > 0;
1000 if (!cJSON_IsObject(je))
return false;
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");
1009 if (!cJSON_IsString(jTitle) || !jTitle->valuestring || jTitle->valuestring[0] ==
'\0') {
1010 LOG_W(
TAG,
"Password import: skipping entry with no title");
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';
1021 copyField(entry.
title,
sizeof(entry.
title), jTitle);
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)
1031 uint16_t existingSlot = 0;
1032 if (store.findByTitle(entry.
title, &existingSlot)) {
1033 return store.updateEntry(existingSlot, entry);
1035 return store.addEntry(entry);
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);
1058 const cJSON* entries = cJSON_GetObjectItemCaseSensitive(in,
"entries");
1069 auto&
module = cdc::mod_password::PasswordModule::instance();
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,...)
#define LOG_I(tag, fmt,...)
const char * getName() const override
Returns the module name supplied to the constructor.
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.
uint16_t capacity() const
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
static I18n & instance()
Singleton accessor.
void registerEnglishTable(const I18nEntry *entries, std::size_t count)
Append English entries to the lookup table.
static constexpr uint16_t MAX_TEXT_LEN
static ViewStack & instance()
Returns singleton view-stack instance.
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.
IKeyboardProvider * getKeyboard()
void unescapeSpaces(char *s)
Replaces every \ escape sequence with a single space character in-place.
const char * nextToken(const char *s, char *out, size_t outSize)
Extracts one whitespace-delimited token from a string.
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.
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.
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.
const char * tr(const char *key)
Look up a translation by string key.
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().
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.