feat(lite): backup & keys UI — export seed/keys + import (Settings)
Add a "Backup & keys" section to the lite Settings page, shown only for an open wallet, wiring the M4 controller backup/import surface into the GUI: - "Show seed" / "Show private keys" -> exportSeed() / exportPrivateKeys(); the revealed secret is displayed read-only (TextWrapped, no extra copies) with Copy and "Hide & wipe" controls. - "Import key" (password input) -> importKey() (auto-detects WIF vs shielded); do_import_sk just records the key + saves (no synchronous rescan), so this is safe on the UI thread — history appears after the next sync. Secret hygiene: the revealed-backup buffer is sodium-wiped via secureWipeLiteSecret on hide, on a new export (overwrite), and if the wallet closes while revealed; each export also wipes the controller's result copy; the import input buffer is zeroed immediately after submission. Lite app + full-node variant build/link clean; controller methods already covered by testLiteWalletControllerM4; hygiene clean. GUI behavior itself isn't auto-verifiable here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,13 @@ struct SettingsPageState {
|
|||||||
bool lite_restore_overwrite = false;
|
bool lite_restore_overwrite = false;
|
||||||
std::string lite_lifecycle_status;
|
std::string lite_lifecycle_status;
|
||||||
std::string lite_lifecycle_summary;
|
std::string lite_lifecycle_summary;
|
||||||
|
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
|
||||||
|
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
|
||||||
|
// import. lite_import_key is the import input buffer (wiped right after submission).
|
||||||
|
std::string lite_export_secret;
|
||||||
|
std::string lite_export_label;
|
||||||
|
char lite_import_key[512] = "";
|
||||||
|
std::string lite_backup_status;
|
||||||
bool mine_when_idle = false;
|
bool mine_when_idle = false;
|
||||||
int mine_idle_delay = 120;
|
int mine_idle_delay = 120;
|
||||||
bool idle_thread_scaling = false;
|
bool idle_thread_scaling = false;
|
||||||
@@ -1724,6 +1731,89 @@ void RenderSettingsPage(App* app) {
|
|||||||
s_settingsState.lite_lifecycle_summary.c_str());
|
s_settingsState.lite_lifecycle_summary.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Backup & keys (open wallet only) ----------------------------------
|
||||||
|
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().text(TypeStyle::Body2, "Backup & keys");
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
auto r = app->liteWallet()->exportSeed();
|
||||||
|
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_backup_status.clear();
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
s_settingsState.lite_backup_status = r.error;
|
||||||
|
}
|
||||||
|
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
auto r = app->liteWallet()->exportPrivateKeys();
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
if (r.ok) {
|
||||||
|
s_settingsState.lite_export_secret = r.privateKeysJson;
|
||||||
|
s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds";
|
||||||
|
s_settingsState.lite_backup_status.clear();
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
s_settingsState.lite_backup_status = r.error;
|
||||||
|
}
|
||||||
|
wallet::secureWipeLiteSecret(r.privateKeysJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
|
||||||
|
if (!s_settingsState.lite_export_secret.empty()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_export_label.c_str());
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
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());
|
||||||
|
if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import a spending/viewing key (history appears after the next sync).
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Import key");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
||||||
|
sizeof(s_settingsState.lite_import_key),
|
||||||
|
ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
|
||||||
|
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
||||||
|
s_settingsState.lite_backup_status =
|
||||||
|
r.ok ? "Key imported — run a sync to scan its history" : r.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s_settingsState.lite_backup_status.empty()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_backup_status.c_str());
|
||||||
|
}
|
||||||
|
} else if (!s_settingsState.lite_export_secret.empty()) {
|
||||||
|
// Wallet closed while a backup was revealed — don't leave it in memory.
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
float rpcLblW = std::max(
|
float rpcLblW = std::max(
|
||||||
|
|||||||
Reference in New Issue
Block a user