From 9569b0ba439bc3379bb210fbba0b46b6f35c52c8 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 5 Jun 2026 17:55:09 -0500 Subject: [PATCH] =?UTF-8?q?feat(lite):=20encryption=20UI=20=E2=80=94=20enc?= =?UTF-8?q?rypt/unlock/lock/decrypt=20in=20Settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Security" subsection to Settings → Backup & keys (open wallet only) that wires the encryption controller methods to the UI: - Unencrypted wallet: passphrase field + "Encrypt wallet". - Encrypted + locked: "Unlock" (passphrase) ; Encrypted + unlocked: "Lock now". - Encrypted: passphrase + "Remove encryption" (decrypt). - Status line reflects the result; state shown from WalletState.isEncrypted()/ isLocked() (kept current by the controller's encryptionstatus refresh poll). Secret hygiene: the passphrase inputs (lite_enc_pass / lite_dec_pass) are sodium-zeroed immediately after each action and when the wallet closes while the section was open. Runtime-checked: app auto-opens a wallet and the new encryptionstatus worker poll runs clean (no errors); tests pass; hygiene clean. Follow-ups (not yet): a send-time unlock prompt and a startup lock-screen overlay for an encrypted+locked wallet (today: unlock via Settings; balances remain viewable while locked). Co-Authored-By: Claude Opus 4.8 --- src/ui/pages/settings_page.cpp | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 303f5dc..a01988a 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -128,6 +128,11 @@ struct SettingsPageState { std::string lite_export_label; char lite_import_key[512] = ""; std::string lite_backup_status; + // Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is + // reused for Encrypt (unencrypted wallet) and Unlock (locked wallet); lite_dec_pass for Decrypt. + char lite_enc_pass[128] = ""; + char lite_dec_pass[128] = ""; + std::string lite_encryption_status; bool mine_when_idle = false; int mine_idle_delay = 120; bool idle_thread_scaling = false; @@ -1809,10 +1814,74 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_backup_status.c_str()); } + + // ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ---- + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().text(TypeStyle::Body2, "Security"); + const auto& wstate = app->getWalletState(); + const float encLabelX = leftX - sectionOrigin.x + liteLabelW; + if (!wstate.isEncrypted()) { + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Passphrase"); + ImGui::SameLine(encLabelX); + ImGui::SetNextItemWidth(liteInputW); + ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass, + sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + if (TactileButton("Encrypt wallet##LiteEncrypt", ImVec2(0, 0), S.resolveFont("button"))) { + const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass); + sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); + s_settingsState.lite_encryption_status = r.ok ? "Wallet encrypted" : r.error; + } + } else { + if (wstate.isLocked()) { + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Unlock"); + ImGui::SameLine(encLabelX); + ImGui::SetNextItemWidth(liteInputW); + ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass, + sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + if (TactileButton("Unlock##LiteUnlock", ImVec2(0, 0), S.resolveFont("button"))) { + const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass); + sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); + s_settingsState.lite_encryption_status = ok ? "Wallet unlocked" : "Unlock failed"; + } + } else { + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) { + s_settingsState.lite_encryption_status = + app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed"; + } + } + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Passphrase"); + ImGui::SameLine(encLabelX); + ImGui::SetNextItemWidth(liteInputW); + ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass, + sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + if (TactileButton("Remove encryption##LiteDecrypt", ImVec2(0, 0), S.resolveFont("button"))) { + const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass); + sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); + s_settingsState.lite_encryption_status = r.ok ? "Encryption removed" : r.error; + } + } + if (!s_settingsState.lite_encryption_status.empty()) { + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), + s_settingsState.lite_encryption_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 closed while a backup/secret was revealed — don't leave it in memory. wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); + sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); + sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); } } else {