feat(settings): redesign the remove-encryption (decrypt) modal onto reference design

Migrate App::renderDecryptWalletDialog ("Remove Wallet Encryption") onto the
settings-modal reference design — presentation-only for a fund-critical dialog,
plus one deliberate secret-hygiene addition:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 480,
  idSuffix "decrypt"}) replacing the legacy positional overload
- Phase 0 amber TextWrapped warning -> material::DialogWarningHeader(decrypt_warning)
- drop the redundant Phase 0 Separator
- every rendered-chrome string across all 4 phases (PassphraseEntry / Working /
  Success / Error) -> TR() (14 new decrypt_*/try_again keys added to the English
  source + all 8 res/lang/*.json additively; the button reuses
  settings_remove_encryption, the label reuses change_pass_current, cancel/close reused)
- HARDENING: wipe decrypt_pass_buf_ on BlurFloat dismiss (X/Esc/outside-click),
  mirroring the change-passphrase fix; canClose() gates pOpen so this cannot fire
  during the Working phase

The fund-moving async worker pyramid (unlock -> export -> stop -> backup ->
restart -> re-import, vault/PIN cleanup) is byte-for-byte unchanged; the three
in-worker toast notifications and the "Incorrect passphrase" status are left in
English to avoid touching the fund path (a separate service-layer i18n pass).

Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format
specifiers and the wallet.dat.encrypted.bak literal preserved; a 3-lens adversarial
review (secret-hygiene / fund-path-untouched / ui-i18n) returned zero findings, with
the fund-path lens proving the async pyramid diffs IDENTICAL against HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 12:02:11 -05:00
parent 5985f05fb2
commit 90df19b8b6
10 changed files with 158 additions and 31 deletions

View File

@@ -1434,26 +1434,21 @@ void App::renderDecryptWalletDialog() {
bool canClose = wallet_security_workflow_.canClose();
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
OverlayDialogSpec ov;
ov.title = TR("decrypt_title"); ov.p_open = pOpen;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 480.0f; ov.idSuffix = "decrypt";
if (BeginOverlayDialog(ov)) {
// ---- Phase 0: Passphrase entry ----
if (decryptState.phase == DecryptPhase::PassphraseEntry) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
ImGui::TextWrapped(ICON_MD_WARNING
" This will remove encryption from your wallet. "
"Your private keys will be stored unprotected on disk.");
ImGui::PopStyleColor();
DialogWarningHeader(TR("decrypt_warning"));
ImGui::Spacing();
ImGui::TextWrapped(
"The wallet will be exported, the daemon restarted with a fresh "
"unencrypted wallet, and all keys re-imported. This may take "
"several minutes depending on wallet size.");
ImGui::Spacing();
ImGui::Separator();
ImGui::TextWrapped("%s", TR("decrypt_desc"));
ImGui::Spacing();
ImGui::Text("Current Passphrase:");
ImGui::TextUnformatted(TR("change_pass_current"));
ImGui::PushItemWidth(-1);
bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
@@ -1469,7 +1464,7 @@ void App::renderDecryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!valid || decryptState.inProgress);
if (ui::material::TactileButton("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
std::string passphrase(decrypt_pass_buf_);
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
wallet_security_workflow_.start(std::chrono::steady_clock::now());
@@ -1660,7 +1655,7 @@ void App::renderDecryptWalletDialog() {
ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
show_decrypt_dialog_ = false;
}
@@ -1669,11 +1664,11 @@ void App::renderDecryptWalletDialog() {
} else if (decryptState.phase == DecryptPhase::Working) {
// Step checklist
const char* stepLabels[] = {
"Unlocking wallet",
"Exporting wallet keys",
"Stopping daemon",
"Backing up encrypted wallet",
"Restarting daemon"
TR("decrypt_step_unlock"),
TR("decrypt_step_export"),
TR("decrypt_step_stop"),
TR("decrypt_step_backup"),
TR("decrypt_step_restart")
};
const int numSteps = 5;
@@ -1750,10 +1745,9 @@ void App::renderDecryptWalletDialog() {
// Step-specific hints
if (decryptState.step == DecryptStep::RestartDaemon) {
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
ImGui::TextWrapped("%s", TR("decrypt_wait_restart"));
} else {
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
"and re-importing. This may take several minutes.");
ImGui::TextWrapped("%s", TR("decrypt_wait_general"));
}
// Total elapsed
@@ -1770,15 +1764,13 @@ void App::renderDecryptWalletDialog() {
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!");
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "%s", TR("decrypt_success_title"));
ImGui::Spacing();
ImGui::TextWrapped(
"Your wallet is now unencrypted. A backup of the encrypted wallet "
"was saved as wallet.dat.encrypted.bak in your data directory.");
ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
ImGui::Spacing();
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) {
if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
show_decrypt_dialog_ = false;
}
@@ -1788,23 +1780,31 @@ void App::renderDecryptWalletDialog() {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed");
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", TR("decrypt_error_title"));
ImGui::Spacing();
ImGui::TextWrapped("%s", decryptState.status.c_str());
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ui::material::TactileButton("Try Again", ImVec2(btnW, 40))) {
if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
wallet_security_workflow_.reset();
}
ImGui::SameLine();
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) {
if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
show_decrypt_dialog_ = false;
}
}
EndOverlayDialog();
}
// Wipe the passphrase buffer if the dialog was dismissed (X / Esc / outside-
// click) without submitting. The submit and Cancel paths already memset it;
// this covers the BlurFloat dismiss paths. (canClose is false during Working,
// so an in-flight decrypt cannot be dismissed here.)
if (!show_decrypt_dialog_) {
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
}
}
// ===========================================================================

View File

@@ -522,6 +522,21 @@ void I18n::loadBuiltinEnglish()
strings_["change_pass_current"] = "Current Passphrase:";
strings_["change_pass_new"] = "New Passphrase:";
strings_["change_pass_confirm"] = "Confirm New:";
// --- Remove-encryption (decrypt) dialog (settings) ---
strings_["decrypt_title"] = "Remove Wallet Encryption";
strings_["decrypt_warning"] = "This will remove encryption from your wallet. Your private keys will be stored unprotected on disk.";
strings_["decrypt_desc"] = "The wallet will be exported, the daemon restarted with a fresh unencrypted wallet, and all keys re-imported. This may take several minutes depending on wallet size.";
strings_["decrypt_step_unlock"] = "Unlocking wallet";
strings_["decrypt_step_export"] = "Exporting wallet keys";
strings_["decrypt_step_stop"] = "Stopping daemon";
strings_["decrypt_step_backup"] = "Backing up encrypted wallet";
strings_["decrypt_step_restart"] = "Restarting daemon";
strings_["decrypt_wait_restart"] = "Waiting for the daemon to finish starting up...";
strings_["decrypt_wait_general"] = "Please wait. The daemon is exporting keys, restarting, and re-importing. This may take several minutes.";
strings_["decrypt_success_title"] = "Wallet decrypted successfully!";
strings_["decrypt_success_desc"] = "Your wallet is now unencrypted. A backup of the encrypted wallet was saved as wallet.dat.encrypted.bak in your data directory.";
strings_["decrypt_error_title"] = "Decryption failed";
strings_["try_again"] = "Try Again";
strings_["wiz_confirm"] = "Confirm:";
strings_["wiz_strength_weak"] = "Weak";
strings_["wiz_strength_fair"] = "Fair";