diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 003c1c9..0e9aaa8 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -120,6 +120,8 @@ struct SettingsPageState { // import. lite_import_key is the import input buffer (wiped right after submission). std::string lite_export_secret; std::string lite_export_label; + bool lite_export_is_seed = false; // revealed secret is a seed (show birthday + save-to-file) + unsigned long long lite_export_birthday = 0; // seed birthday (shown with the seed) char lite_import_key[512] = ""; std::string lite_backup_status; // Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is @@ -145,6 +147,18 @@ struct SettingsPageState { static SettingsPageState s_settingsState; +// Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input. +static int liteSeedWordCount(const char* s) { + int words = 0; + bool inWord = false; + for (; s && *s; ++s) { + const bool space = std::isspace(static_cast(*s)) != 0; + if (space) inWord = false; + else if (!inWord) { inWord = true; ++words; } + } + return words; +} + static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() { switch (s_settingsState.lite_lifecycle_operation) { case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting; @@ -205,6 +219,18 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) { } } liteSecretScrubber{input}; + // Restore needs a complete 24-word seed; reject early (the scrubber above still wipes the + // entered secret on this return path). + if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) { + const int words = liteSeedWordCount(s_settingsState.lite_restore_seed); + if (words != 24) { + s_settingsState.lite_lifecycle_status = + "Enter all 24 seed words to restore (got " + std::to_string(words) + ")"; + s_settingsState.lite_lifecycle_summary.clear(); + return; + } + } + // When a linked lite backend is present, execute the operation for real through the // App-owned controller. Otherwise fall back to the validation-only adapter. if (auto* lite = app->liteWallet()) { @@ -1531,6 +1557,14 @@ void RenderSettingsPage(App* app) { ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed, sizeof(s_settingsState.lite_restore_seed), ImGuiInputTextFlags_Password); + // Live 24-word count so the user knows the seed is complete before restoring. + { + const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed); + char wbuf[32]; + snprintf(wbuf, sizeof(wbuf), "%d / 24 words", wc); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf); + } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); @@ -1539,6 +1573,9 @@ void RenderSettingsPage(App* app) { ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday); if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0; + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), + "Block height to start scanning from. Leave 0 if unknown (slower full scan)."); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); @@ -1591,10 +1628,15 @@ void RenderSettingsPage(App* app) { wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); if (r.ok) { s_settingsState.lite_export_secret = r.seedPhrase; - s_settingsState.lite_export_label = "Seed phrase — write it down, never share it"; + s_settingsState.lite_export_label = + "Seed phrase — the ONLY way to restore your wallet. " + "Write it down, store it offline, never share it."; + s_settingsState.lite_export_is_seed = true; + s_settingsState.lite_export_birthday = r.birthday; s_settingsState.lite_backup_status.clear(); } else { s_settingsState.lite_export_label.clear(); + s_settingsState.lite_export_is_seed = false; s_settingsState.lite_backup_status = r.error; } wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy @@ -1603,6 +1645,7 @@ void RenderSettingsPage(App* app) { if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) { auto r = app->liteWallet()->exportPrivateKeys(); wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); + s_settingsState.lite_export_is_seed = false; // keys, not a seed if (r.ok) { s_settingsState.lite_export_secret = r.privateKeysJson; s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds"; @@ -1623,14 +1666,37 @@ void RenderSettingsPage(App* app) { ImGui::PushTextWrapPos(0.0f); ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str()); ImGui::PopTextWrapPos(); + // The seed's birthday is needed to restore quickly — show + back it up too. + if (s_settingsState.lite_export_is_seed) { + char bday[64]; + snprintf(bday, sizeof(bday), "Birthday: %llu (back this up too)", + s_settingsState.lite_export_birthday); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday); + } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) { ImGui::SetClipboardText(s_settingsState.lite_export_secret.c_str()); } ImGui::SameLine(0, Layout::spacingSm()); + // Save the seed (+ birthday) to an owner-only (0600) file in the config dir. + if (s_settingsState.lite_export_is_seed && + TactileButton("Save to file##LiteExportSave", ImVec2(0, 0), S.resolveFont("button"))) { + std::string path = util::Platform::getConfigDir() + "/lite-seed-backup.txt"; + std::string content = s_settingsState.lite_export_secret + "\nBirthday: " + + std::to_string(s_settingsState.lite_export_birthday) + "\n"; + const bool ok = util::Platform::writeFileAtomically( + path, content, /*restrictPermissions=*/true); + wallet::secureWipeLiteSecret(content); + s_settingsState.lite_backup_status = ok + ? ("Saved (plaintext, owner-only) to " + path) + : ("Could not write " + path); + } + if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) { wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); + s_settingsState.lite_export_is_seed = false; } }