From 46f7da4fac7a13f09e18f095033f678c91256f9e Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 14 Jul 2026 13:29:02 -0500 Subject: [PATCH] feat(settings): redesign the PIN setup/change/remove modals onto reference design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the three PIN dialogs in App::renderPinDialogs (Set PIN / Change PIN / Remove PIN) onto the settings-modal reference design — presentation-only, plus a deliberate secret-hygiene addition: - struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 420/420/400, idSuffix pinsetup/pinchange/pinremove}) replacing the legacy positional overload - drop each dialog's redundant post-description Separator - every rendered-chrome string -> TR(): titles/buttons reuse settings_set_pin / settings_change_pin / settings_remove_pin, the Set-PIN confirm reuses wiz_pin_confirm; 7 new pin_* keys added to the English source + all 8 res/lang/*.json additively; ImGui::Text -> TextUnformatted for labels - HARDENING: wipe the passphrase/PIN input buffers on BlurFloat dismiss (X/Esc/outside-click) for all three dialogs. Set PIN's wallet-passphrase buffer (pin_passphrase_buf_) was previously left resident on dismiss; it is now zeroed. The vault/RPC path is byte-for-byte unchanged: the submit-time memsets, the walletpassphrase/walletlock RPCs, vault_->store/changePin/retrieve/removeVault, SecureVault::secureZero, and setPinEnabled/save are untouched. The pin_status_ and toast strings inside the worker callbacks are left in English (a separate service-layer i18n pass), to avoid editing the secret path. Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format specifiers; a 3-lens adversarial review (secret-hygiene / presentation-only / ui-i18n) returned zero findings, with the presentation lens proving the vault path diffs byte-for-byte identical against HEAD. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/lang/de.json | 7 ++++ res/lang/es.json | 7 ++++ res/lang/fr.json | 7 ++++ res/lang/ja.json | 7 ++++ res/lang/ko.json | 7 ++++ res/lang/pt.json | 7 ++++ res/lang/ru.json | 7 ++++ res/lang/zh.json | 7 ++++ src/app_security.cpp | 78 +++++++++++++++++++++++++++----------------- src/util/i18n.cpp | 8 +++++ 10 files changed, 112 insertions(+), 30 deletions(-) diff --git a/res/lang/de.json b/res/lang/de.json index 53f19f7..a52a614 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -874,7 +874,14 @@ "peers_upper": "PEERS", "peers_version": "Version", "pending": "Ausstehend", + "pin_change_desc": "Ändern Sie Ihre Entsperr-PIN. Sie benötigen Ihre aktuelle PIN und eine neue PIN.", + "pin_confirm_new_label": "Neue PIN bestätigen:", + "pin_current_label": "Aktuelle PIN:", + "pin_new_label": "Neue PIN (4-8 Ziffern):", "pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.", + "pin_remove_desc": "Geben Sie Ihre aktuelle PIN ein, um das Entfernen zu bestätigen. Sie benötigen dann Ihre vollständige Passphrase zum Entsperren.", + "pin_setup_desc": "Legen Sie eine 4-8-stellige PIN für die schnelle Wallet-Entsperrung fest. Ihre Wallet-Passphrase wird mit dieser PIN verschlüsselt und lokal gespeichert.", + "pin_wallet_passphrase": "Wallet-Passphrase:", "ping": "Ping", "portfolio_add_entry": "Eintrag hinzufügen", "portfolio_add_to": "Zum Portfolio hinzufügen", diff --git a/res/lang/es.json b/res/lang/es.json index e117346..7c7ed4a 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -874,7 +874,14 @@ "peers_upper": "NODOS", "peers_version": "Versión", "pending": "Pendiente", + "pin_change_desc": "Cambia tu PIN de desbloqueo. Necesitas tu PIN actual y un nuevo PIN.", + "pin_confirm_new_label": "Confirmar nuevo PIN:", + "pin_current_label": "PIN actual:", + "pin_new_label": "Nuevo PIN (4-8 dígitos):", "pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.", + "pin_remove_desc": "Introduce tu PIN actual para confirmar la eliminación. Necesitarás tu frase de contraseña completa para desbloquear.", + "pin_setup_desc": "Establece un PIN de 4-8 dígitos para desbloquear el monedero rápidamente. Tu frase de contraseña se cifrará con este PIN y se almacenará localmente.", + "pin_wallet_passphrase": "Frase de contraseña del monedero:", "ping": "Ping", "portfolio_add_entry": "Añadir entrada", "portfolio_add_to": "Agregar a la cartera de inversiones", diff --git a/res/lang/fr.json b/res/lang/fr.json index 20a7735..a3cdc25 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -874,7 +874,14 @@ "peers_upper": "PAIRS", "peers_version": "Version", "pending": "En attente", + "pin_change_desc": "Modifiez votre code PIN de déverrouillage. Vous avez besoin de votre code PIN actuel et d'un nouveau code PIN.", + "pin_confirm_new_label": "Confirmer le nouveau code PIN :", + "pin_current_label": "Code PIN actuel :", + "pin_new_label": "Nouveau code PIN (4 à 8 chiffres) :", "pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.", + "pin_remove_desc": "Saisissez votre code PIN actuel pour confirmer la suppression. Vous devrez utiliser votre phrase secrète complète pour déverrouiller.", + "pin_setup_desc": "Définissez un code PIN de 4 à 8 chiffres pour déverrouiller rapidement votre portefeuille. Votre phrase secrète sera chiffrée avec ce code PIN et stockée localement.", + "pin_wallet_passphrase": "Phrase secrète du portefeuille :", "ping": "Ping", "portfolio_add_entry": "Ajouter une entrée", "portfolio_add_to": "Ajouter au portefeuille", diff --git a/res/lang/ja.json b/res/lang/ja.json index e937ee9..e815a8f 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -874,7 +874,14 @@ "peers_upper": "ピア", "peers_version": "バージョン", "pending": "保留中", + "pin_change_desc": "ロック解除PINを変更します。現在のPINと新しいPINが必要です。", + "pin_confirm_new_label": "新しいPINの確認:", + "pin_current_label": "現在のPIN:", + "pin_new_label": "新しいPIN(4〜8桁):", "pin_not_set": "PINが設定されていません。パスフレーズで解除してください。", + "pin_remove_desc": "削除を確認するには現在のPINを入力してください。ロック解除には完全なパスフレーズが必要になります。", + "pin_setup_desc": "ウォレットをすばやくロック解除するための4〜8桁のPINを設定します。ウォレットのパスフレーズはこのPINで暗号化され、ローカルに保存されます。", + "pin_wallet_passphrase": "ウォレットのパスフレーズ:", "ping": "Ping", "portfolio_add_entry": "エントリを追加", "portfolio_add_to": "ポートフォリオに追加", diff --git a/res/lang/ko.json b/res/lang/ko.json index 2833616..8bc457d 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -874,7 +874,14 @@ "peers_upper": "피어", "peers_version": "버전", "pending": "대기 중", + "pin_change_desc": "잠금 해제 PIN을 변경합니다. 현재 PIN과 새 PIN이 필요합니다.", + "pin_confirm_new_label": "새 PIN 확인:", + "pin_current_label": "현재 PIN:", + "pin_new_label": "새 PIN(4~8자리):", "pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.", + "pin_remove_desc": "삭제를 확인하려면 현재 PIN을 입력하세요. 잠금 해제에는 전체 암호가 필요합니다.", + "pin_setup_desc": "지갑을 빠르게 잠금 해제할 4~8자리 PIN을 설정하세요. 지갑 암호가 이 PIN으로 암호화되어 로컬에 저장됩니다.", + "pin_wallet_passphrase": "지갑 암호:", "ping": "Ping", "portfolio_add_entry": "항목 추가", "portfolio_add_to": "포트폴리오에 추가", diff --git a/res/lang/pt.json b/res/lang/pt.json index 88dd9a6..44f5856 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -874,7 +874,14 @@ "peers_upper": "PARES", "peers_version": "Versão", "pending": "Pendente", + "pin_change_desc": "Altere seu PIN de desbloqueio. Você precisa do seu PIN atual e de um novo PIN.", + "pin_confirm_new_label": "Confirmar novo PIN:", + "pin_current_label": "PIN atual:", + "pin_new_label": "Novo PIN (4 a 8 dígitos):", "pin_not_set": "PIN não definido. Use a senha para desbloquear.", + "pin_remove_desc": "Digite seu PIN atual para confirmar a remoção. Você precisará usar sua senha completa para desbloquear.", + "pin_setup_desc": "Defina um PIN de 4 a 8 dígitos para desbloquear a carteira rapidamente. Sua senha da carteira será criptografada com este PIN e armazenada localmente.", + "pin_wallet_passphrase": "Senha da carteira:", "ping": "Ping", "portfolio_add_entry": "Adicionar entrada", "portfolio_add_to": "Adicionar ao portfólio", diff --git a/res/lang/ru.json b/res/lang/ru.json index c1a9c66..a45e3ac 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -874,7 +874,14 @@ "peers_upper": "УЗЛЫ", "peers_version": "Версия", "pending": "Ожидание", + "pin_change_desc": "Измените PIN-код разблокировки. Вам понадобится текущий PIN-код и новый PIN-код.", + "pin_confirm_new_label": "Подтвердите новый PIN-код:", + "pin_current_label": "Текущий PIN-код:", + "pin_new_label": "Новый PIN-код (4–8 цифр):", "pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.", + "pin_remove_desc": "Введите текущий PIN-код для подтверждения удаления. Для разблокировки вам понадобится полный пароль.", + "pin_setup_desc": "Установите PIN-код из 4–8 цифр для быстрой разблокировки кошелька. Пароль вашего кошелька будет зашифрован этим PIN-кодом и сохранён локально.", + "pin_wallet_passphrase": "Пароль кошелька:", "ping": "Пинг", "portfolio_add_entry": "Добавить запись", "portfolio_add_to": "Добавить в портфель", diff --git a/res/lang/zh.json b/res/lang/zh.json index 2c63554..b4d4b7c 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -874,7 +874,14 @@ "peers_upper": "节点", "peers_version": "版本", "pending": "待处理", + "pin_change_desc": "更改您的解锁 PIN 码。您需要当前 PIN 码和新 PIN 码。", + "pin_confirm_new_label": "确认新 PIN 码:", + "pin_current_label": "当前 PIN 码:", + "pin_new_label": "新 PIN 码(4-8 位):", "pin_not_set": "未设置 PIN。使用密码解锁。", + "pin_remove_desc": "输入当前 PIN 码以确认移除。解锁时您需要使用完整的密码短语。", + "pin_setup_desc": "设置一个 4-8 位 PIN 码以快速解锁钱包。您的钱包密码短语将使用此 PIN 码加密并存储在本地。", + "pin_wallet_passphrase": "钱包密码短语:", "ping": "延迟", "portfolio_add_entry": "添加条目", "portfolio_add_to": "添加到投资组合", diff --git a/src/app_security.cpp b/src/app_security.cpp index 06c197f..52ab03e 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -1816,29 +1816,28 @@ void App::renderPinDialogs() { // ---- Set PIN dialog ---- if (show_pin_setup_) { - if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) { - - ImGui::TextWrapped( - "Set a 4-8 digit PIN for quick wallet unlock. " - "Your wallet passphrase will be encrypted with this PIN " - "and stored locally."); - ImGui::Spacing(); - ImGui::Separator(); + OverlayDialogSpec ov; + ov.title = TR("settings_set_pin"); ov.p_open = &show_pin_setup_; + ov.style = OverlayStyle::BlurFloat; + ov.cardWidth = 420.0f; ov.idSuffix = "pinsetup"; + if (BeginOverlayDialog(ov)) { + + ImGui::TextWrapped("%s", TR("pin_setup_desc")); ImGui::Spacing(); - ImGui::Text("Wallet Passphrase:"); + ImGui::TextUnformatted(TR("pin_wallet_passphrase")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); - ImGui::Text("New PIN (4-8 digits):"); + ImGui::TextUnformatted(TR("pin_new_label")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); - ImGui::Text("Confirm PIN:"); + ImGui::TextUnformatted(TR("wiz_pin_confirm")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); @@ -1855,7 +1854,7 @@ void App::renderPinDialogs() { strcmp(pin_buf_, pin_confirm_buf_) == 0; ImGui::BeginDisabled(!valid || pin_in_progress_); - if (ui::material::TactileButton("Set PIN", ImVec2(-1, 40))) { + if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) { pin_in_progress_ = true; pin_status_ = "Verifying passphrase..."; @@ -1911,30 +1910,39 @@ void App::renderPinDialogs() { ImGui::EndDisabled(); EndOverlayDialog(); } + // Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc / + // outside-click) without submitting. The submit path already memsets them. + if (!show_pin_setup_) { + memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_)); + memset(pin_buf_, 0, sizeof(pin_buf_)); + memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_)); + } } // ---- Change PIN dialog ---- if (show_pin_change_) { - if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) { - - ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN."); - ImGui::Spacing(); - ImGui::Separator(); + OverlayDialogSpec ov; + ov.title = TR("settings_change_pin"); ov.p_open = &show_pin_change_; + ov.style = OverlayStyle::BlurFloat; + ov.cardWidth = 420.0f; ov.idSuffix = "pinchange"; + if (BeginOverlayDialog(ov)) { + + ImGui::TextWrapped("%s", TR("pin_change_desc")); ImGui::Spacing(); - ImGui::Text("Current PIN:"); + ImGui::TextUnformatted(TR("pin_current_label")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); - ImGui::Text("New PIN (4-8 digits):"); + ImGui::TextUnformatted(TR("pin_new_label")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); - ImGui::Text("Confirm New PIN:"); + ImGui::TextUnformatted(TR("pin_confirm_new_label")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); @@ -1951,7 +1959,7 @@ void App::renderPinDialogs() { strcmp(pin_buf_, pin_confirm_buf_) == 0; ImGui::BeginDisabled(!valid || pin_in_progress_); - if (ui::material::TactileButton("Change PIN", ImVec2(-1, 40))) { + if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) { pin_in_progress_ = true; pin_status_ = "Changing PIN..."; std::string oldPin(pin_old_buf_); @@ -1984,20 +1992,26 @@ void App::renderPinDialogs() { ImGui::EndDisabled(); EndOverlayDialog(); } + // Wipe the PIN buffers if the dialog was dismissed without submitting. + if (!show_pin_change_) { + memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); + memset(pin_buf_, 0, sizeof(pin_buf_)); + memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_)); + } } // ---- Remove PIN dialog ---- if (show_pin_remove_) { - if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) { - - ImGui::TextWrapped( - "Enter your current PIN to confirm removal. " - "You will need to use your full passphrase to unlock."); - ImGui::Spacing(); - ImGui::Separator(); + OverlayDialogSpec ov; + ov.title = TR("settings_remove_pin"); ov.p_open = &show_pin_remove_; + ov.style = OverlayStyle::BlurFloat; + ov.cardWidth = 400.0f; ov.idSuffix = "pinremove"; + if (BeginOverlayDialog(ov)) { + + ImGui::TextWrapped("%s", TR("pin_remove_desc")); ImGui::Spacing(); - ImGui::Text("Current PIN:"); + ImGui::TextUnformatted(TR("pin_current_label")); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); @@ -2010,7 +2024,7 @@ void App::renderPinDialogs() { ImGui::Spacing(); bool valid = strlen(pin_old_buf_) >= 4; ImGui::BeginDisabled(!valid || pin_in_progress_); - if (ui::material::TactileButton("Remove PIN", ImVec2(-1, 40))) { + if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) { pin_in_progress_ = true; pin_status_ = "Verifying PIN..."; std::string oldPin(pin_old_buf_); @@ -2047,6 +2061,10 @@ void App::renderPinDialogs() { ImGui::EndDisabled(); EndOverlayDialog(); } + // Wipe the PIN buffer if the dialog was dismissed without submitting. + if (!show_pin_remove_) { + memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); + } } } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 5e64a0f..3d3083a 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -537,6 +537,14 @@ void I18n::loadBuiltinEnglish() 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"; + // --- PIN setup / change / remove dialogs (settings) --- + strings_["pin_setup_desc"] = "Set a 4-8 digit PIN for quick wallet unlock. Your wallet passphrase will be encrypted with this PIN and stored locally."; + strings_["pin_wallet_passphrase"] = "Wallet Passphrase:"; + strings_["pin_new_label"] = "New PIN (4-8 digits):"; + strings_["pin_change_desc"] = "Change your unlock PIN. You need your current PIN and a new PIN."; + strings_["pin_current_label"] = "Current PIN:"; + strings_["pin_confirm_new_label"] = "Confirm New PIN:"; + strings_["pin_remove_desc"] = "Enter your current PIN to confirm removal. You will need to use your full passphrase to unlock."; strings_["wiz_confirm"] = "Confirm:"; strings_["wiz_strength_weak"] = "Weak"; strings_["wiz_strength_fair"] = "Fair";