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.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"},
74 return store.hasSlotRange() && slot < store.capacity();
84 if (!store.hasSlotRange()) {
88 uint16_t cap = store.capacity();
99 if (!store.listEntriesSorted(list.get(), cap, &count)) {
107 for (uint16_t i = 0; i < count; i++) {
117 char slotBuf[8] = {};
118 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
119 if (!p || !slotBuf[0]) {
123 uint16_t slot =
static_cast<uint16_t
>(atoi(slotBuf));
150 return s && s[0] && s[1] ==
'\0' && (s[0] ==
'x' || s[0] ==
'X' || s[0] ==
'-');
158 static const char charset[] =
159 "abcdefghijklmnopqrstuvwxyz"
160 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
163 constexpr size_t charsetLen =
sizeof(charset) - 1;
164 constexpr uint8_t charCount = 16;
165 if (outSize < charCount + 1)
return;
167 uint8_t rand[charCount] = {};
169 bool gotRand = se && se->getRandom(rand,
sizeof(rand));
171 for (uint8_t i = 0; i < charCount; i++) {
172 rand[i] =
static_cast<uint8_t
>(esp_random() & 0xFF);
176 for (uint8_t i = 0; i < charCount; i++) {
177 out[i] = charset[rand[i] % charsetLen];
179 out[charCount] =
'\0';
186 char slotBuf[8] = {};
191 char totpBuf[8] = {};
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";
200 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
201 if (!p || !slotBuf[0]) {
206 p =
nextToken(p, username,
sizeof(username));
207 p =
nextToken(p, password,
sizeof(password));
209 p =
nextToken(p, totpBuf,
sizeof(totpBuf));
210 if (!title[0] || !username[0] || !password[0] || !url[0] || !totpBuf[0]) {
221 if (!store.findFreeLogicalSlot(&slot)) {
226 slot =
static_cast<uint16_t
>(atoi(slotBuf));
233 strncpy(entry.
title, title,
sizeof(entry.
title) - 1);
238 char generatedPassword[40] = {};
239 if (password[0] ==
'x' && password[1] ==
'\0') {
247 strncpy(entry.
url, url,
sizeof(entry.
url) - 1);
250 if (totpBuf[0] !=
'-' || totpBuf[1] !=
'\0') {
251 int totp = atoi(totpBuf);
252 if (totp < 0 || totp > 254) {
256 entry.
totpSlot =
static_cast<uint8_t
>(totp);
259 if (notes && notes[0]) {
260 strncpy(entry.
notes, notes,
sizeof(entry.
notes) - 1);
264 bool ok = store.updateEntry(slot, entry);
266 if (generatedPassword[0]) {
281 char slotBuf[8] = {};
284 "Usage: PASSWORD EDIT <slot> <title|username|password|url|totp|notes> <value>\r\n"
285 " Use '\\\\ ' for spaces inside values.\r\n";
287 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
288 if (!p || !slotBuf[0]) {
298 uint16_t slot =
static_cast<uint16_t
>(atoi(slotBuf));
311 if (!value || !value[0]) {
316 if (strcasecmp(field,
"title") == 0) {
322 strncpy(entry.
title, value,
sizeof(entry.
title) - 1);
324 }
else if (strcasecmp(field,
"username") == 0) {
332 }
else if (strcasecmp(field,
"password") == 0) {
340 }
else if (strcasecmp(field,
"url") == 0) {
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) {
354 entry.
totpSlot =
static_cast<uint8_t
>(totp);
355 }
else if (strcasecmp(field,
"notes") == 0) {
361 strncpy(entry.
notes, value,
sizeof(entry.
notes) - 1);
377 char slotBuf[8] = {};
378 const char* p =
nextToken(args, slotBuf,
sizeof(slotBuf));
379 if (!p || !slotBuf[0]) {
383 uint16_t slot =
static_cast<uint16_t
>(atoi(slotBuf));
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},
398 {
nullptr,
nullptr,
nullptr,
nullptr},
413 reg.registerCommand({
"PASSWORD",
414 "Password vault: LIST/GET/ADD/EDIT/DEL",
463 if (cap == 0)
return false;
496 "Password list allocation failed");
507 uint16_t idx =
static_cast<uint16_t
>(i + 1);
528 if (kb && kb->isConnected()) {
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);
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;
568 snprintf(detailText,
sizeof(detailText),
590 s_infoView.setYesNoCallbacks(
nullptr,
nullptr,
nullptr);
627 s_t9Input.init(title, initialText, maxLen);
675 strncpy(
s_wizard.entry.title, text ? text :
"",
sizeof(
s_wizard.entry.title) - 1);
685 strncpy(
s_wizard.entry.username, text ? text :
"",
sizeof(
s_wizard.entry.username) - 1);
688 s_t9Input.setHint(
"x=Random Y=OK N=Back");
699 if (text && text[0] ==
'x' && text[1] ==
'\0') {
700 char generated[40] = {};
702 strncpy(
s_wizard.entry.password, generated,
sizeof(
s_wizard.entry.password) - 1);
704 strncpy(
s_wizard.entry.password, text ? text :
"",
sizeof(
s_wizard.entry.password) - 1);
715 strncpy(
s_wizard.entry.url, text ? text :
"",
sizeof(
s_wizard.entry.url) - 1);
718 char totpBuf[8] = {};
720 snprintf(totpBuf,
sizeof(totpBuf),
"%u",
s_wizard.entry.totpSlot);
730 if (!text || !text[0]) {
733 int value = atoi(text);
734 if (value < 0 || value > 254) {
739 s_wizard.entry.totpSlot =
static_cast<uint8_t
>(value);
750 strncpy(
s_wizard.entry.notes, text ? text :
"",
sizeof(
s_wizard.entry.notes) - 1);
774 uint16_t slot = *
static_cast<uint16_t*
>(userData);
790 static uint16_t slot = 0;
842 static PasswordModule inst;
851 LOG_I(
TAG,
"Initializing Password module");
856 if (slotRange_.hasRmem) {
902 if (!items || maxItems == 0)
return 0;
935 if (!out)
return false;
938 if (!store.hasSlotRange())
return false;
940 cJSON_AddNumberToObject(out,
"schema_ver",
kSchemaVer);
941 cJSON* entries = cJSON_AddArrayToObject(out,
"entries");
942 if (!entries)
return false;
947 } ctx = { entries, 0 };
950 auto* c =
static_cast<ExportCtx*
>(user);
953 uint16_t logical = 0;
954 if (!store.toLogicalSlot(slot, &logical))
return;
957 if (!store.readEntry(logical, &entry))
return;
959 cJSON* obj = cJSON_CreateObject();
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);
969 cJSON_AddItemToArray(c->arr, obj);
979 return ctx.count > 0;
996 if (!cJSON_IsObject(je))
return false;
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");
1005 if (!cJSON_IsString(jTitle) || !jTitle->valuestring || jTitle->valuestring[0] ==
'\0') {
1006 LOG_W(
TAG,
"Password import: skipping entry with no title");
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';
1017 copyField(entry.
title,
sizeof(entry.
title), jTitle);
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)
1027 uint16_t existingSlot = 0;
1028 if (store.findByTitle(entry.
title, &existingSlot)) {
1029 return store.updateEntry(existingSlot, entry);
1031 return store.addEntry(entry);
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);
1054 const cJSON* entries = cJSON_GetObjectItemCaseSensitive(in,
"entries");
1065 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.