fix(lite): require a confirmed 8+ character passphrase to encrypt
The Lite "Encrypt wallet" field and the Create/Restore passphrase had no confirmation and no minimum length; only an empty passphrase was refused. Encrypting locks the wallet at once, and a locked wallet cannot reveal its seed, so a typo there could cost a user without a seed backup their funds. The full-node encrypt dialog already requires 8 characters and a confirmation. This applies to new passphrases only. Opening and unlocking are unchanged: an existing wallet with a shorter passphrase still opens and unlocks. - A pure liteEncryptPassphraseValid(pass, confirm): at least 8 bytes and equal to the confirmation. It takes string_views, so the page's secret buffers are never copied. - liteLifecyclePassphraseAcceptable(op, pass, confirm) gates the Settings create/open/restore request. Open is never checked, because its passphrase field also unlocks an existing wallet. Create and Restore are checked only when a passphrase was entered; an empty one still creates an unencrypted wallet. - Encrypt gets a Confirm field. The button stays disabled until the passphrase is valid, and a hint says why (too short, with the count, or not matching). - Create/Restore show a Confirm row once a passphrase is typed, with the same hint. Validate stays disabled while the passphrase is invalid, so a click never wipes a typed seed over a passphrase typo. The request builder re-checks the rule. - The confirmation buffers are wiped with their passphrase: after a run or an encrypt, and when the Confirm row is hidden. - Once the wallet is closed, the Settings page wipes the Encrypt, Confirm and Decrypt passphrase buffers whole every time, also when no secret was revealed; before, closing the wallet wiped them only while clearing a revealed backup or secret. They are zeroed in full because an emptied field can still hold the old tail past its first NUL. - New copy under Encrypt: "Anyone with the wallet file can try passphrases offline. Back up your seed phrase first and use a long passphrase." - i18n: three new keys (lite_confirm_label, tt_lite_confirm_pass, lite_encrypt_offline_warning) in i18n.cpp and the 8 language files, added additively. The hints reuse the wizard's wiz_pass_too_short and wiz_pass_mismatch. No new CJK glyphs, so the subset font is unchanged. Tests: the validator rejects empty, short (7) and mismatched or unconfirmed passphrases, and accepts 8 or more with a match. The request gate always accepts Open, even "abc" with no confirmation, accepts Create/Restore without a passphrase, and applies the rule to them otherwise. An encrypted, locked wallet still opens and unlocks through the controller with the 3-character passphrase "abc". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "Geburtstag: %llu (auch diesen sichern)",
|
||||
"lite_birthday_hint": "Blockhöhe, ab der gescannt werden soll. Bei 0 belassen, falls unbekannt (langsamerer vollständiger Scan).",
|
||||
"lite_birthday_label": "Geburtsblock",
|
||||
"lite_confirm_label": "Bestätigen",
|
||||
"lite_console_backend_commands": "Backend-Befehle:",
|
||||
"lite_console_help_passthrough": "Jede andere Eingabe wird als Lite-Wallet-Konsolenbefehl ausgeführt.",
|
||||
"lite_copy": "Kopieren",
|
||||
"lite_could_not_start": "Der Vorgang konnte nicht gestartet werden",
|
||||
"lite_could_not_write": "Konnte nicht schreiben ",
|
||||
"lite_encrypt_offline_warning": "Wer die Wallet-Datei hat, kann Passphrasen offline durchprobieren. Sichern Sie zuerst Ihre Wiederherstellungsphrase und verwenden Sie eine lange Passphrase.",
|
||||
"lite_encrypt_wallet": "Wallet verschlüsseln",
|
||||
"lite_encryption_removed": "Verschlüsselung entfernt",
|
||||
"lite_enter_all_seed_words": "Alle 24 Seed-Wörter zur Wiederherstellung eingeben (%d erhalten)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "Der Daemon wird beim Ausführen des Einrichtungsassistenten gestoppt",
|
||||
"tt_language": "Schnittstellensprache der Wallet-UI",
|
||||
"tt_layout_hotkey": "Hotkey: Links-/Rechts-Pfeiltasten zum Wechseln der Balance-Layouts",
|
||||
"tt_lite_confirm_pass": "Geben Sie die neue Passphrase erneut ein; sie muss übereinstimmen und mindestens 8 Zeichen lang sein",
|
||||
"tt_lite_copy": "Das angezeigte Geheimnis in die Zwischenablage kopieren",
|
||||
"tt_lite_decrypt_pass": "Gib deine Passphrase ein, um die Verschlüsselung von der Wallet zu entfernen",
|
||||
"tt_lite_encrypt": "Die Wallet mit der obigen Passphrase verschlüsseln; sie wird sofort gesperrt und benötigt die Passphrase zum Entsperren",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "Cumpleaños: %llu (respalda esto también)",
|
||||
"lite_birthday_hint": "Altura de bloque desde la que empezar a escanear. Deja 0 si se desconoce (escaneo completo más lento).",
|
||||
"lite_birthday_label": "Fecha de creación",
|
||||
"lite_confirm_label": "Confirmar",
|
||||
"lite_console_backend_commands": "Comandos del backend:",
|
||||
"lite_console_help_passthrough": "Cualquier otra entrada se ejecuta como un comando de consola de la cartera lite.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_start": "No se pudo iniciar la operación",
|
||||
"lite_could_not_write": "No se pudo escribir ",
|
||||
"lite_encrypt_offline_warning": "Cualquiera que tenga el archivo de la cartera puede probar frases de contraseña sin conexión. Haz primero una copia de seguridad de tu frase de recuperación y usa una frase de contraseña larga.",
|
||||
"lite_encrypt_wallet": "Cifrar cartera",
|
||||
"lite_encryption_removed": "Cifrado eliminado",
|
||||
"lite_enter_all_seed_words": "Ingrese las 24 palabras semilla para restaurar (se obtuvieron %d)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "El daemon se detendrá cuando ejecute el asistente de configuración",
|
||||
"tt_language": "Idioma de la interfaz de la billetera",
|
||||
"tt_layout_hotkey": "Atajo: teclas de flecha izquierda/derecha para cambiar diseños de Balance",
|
||||
"tt_lite_confirm_pass": "Escribe de nuevo la nueva frase de contraseña; debe coincidir y tener al menos 8 caracteres",
|
||||
"tt_lite_copy": "Copia el secreto revelado al portapapeles",
|
||||
"tt_lite_decrypt_pass": "Introduce tu frase de contraseña para quitar el cifrado de la cartera",
|
||||
"tt_lite_encrypt": "Cifra la cartera con la frase de contraseña de arriba; se bloquea de inmediato y requiere la frase para desbloquearse",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "Date de création : %llu (à sauvegarder également)",
|
||||
"lite_birthday_hint": "Hauteur de bloc à partir de laquelle commencer l'analyse. Laissez 0 si inconnue (analyse complète plus lente).",
|
||||
"lite_birthday_label": "Bloc de création",
|
||||
"lite_confirm_label": "Confirmer",
|
||||
"lite_console_backend_commands": "Commandes du backend :",
|
||||
"lite_console_help_passthrough": "Toute autre entrée est exécutée comme une commande de la console du portefeuille lite.",
|
||||
"lite_copy": "Copier",
|
||||
"lite_could_not_start": "Impossible de démarrer l'opération",
|
||||
"lite_could_not_write": "Impossible d'écrire ",
|
||||
"lite_encrypt_offline_warning": "Toute personne disposant du fichier du portefeuille peut essayer des phrases secrètes hors ligne. Sauvegardez d'abord votre phrase de récupération et utilisez une phrase secrète longue.",
|
||||
"lite_encrypt_wallet": "Chiffrer le portefeuille",
|
||||
"lite_encryption_removed": "Chiffrement supprimé",
|
||||
"lite_enter_all_seed_words": "Entrez les 24 mots de la phrase de récupération (%d obtenus)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "Le daemon s'arrêtera lors de l'exécution de l'assistant de configuration",
|
||||
"tt_language": "Langue de l'interface du portefeuille",
|
||||
"tt_layout_hotkey": "Raccourci : touches fléchées gauche/droite pour changer les dispositions de Balance",
|
||||
"tt_lite_confirm_pass": "Saisissez à nouveau la nouvelle phrase secrète ; elle doit correspondre et comporter au moins 8 caractères",
|
||||
"tt_lite_copy": "Copie le secret révélé dans le presse-papiers",
|
||||
"tt_lite_decrypt_pass": "Saisissez votre phrase de passe pour retirer le chiffrement du portefeuille",
|
||||
"tt_lite_encrypt": "Chiffre le portefeuille avec la phrase de passe ci-dessus ; il se verrouille immédiatement et requiert la phrase pour se déverrouiller",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||
"lite_birthday_label": "バースデー",
|
||||
"lite_confirm_label": "確認",
|
||||
"lite_console_backend_commands": "バックエンドコマンド:",
|
||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||
"lite_copy": "コピー",
|
||||
"lite_could_not_start": "操作を開始できませんでした",
|
||||
"lite_could_not_write": "書き込めませんでした ",
|
||||
"lite_encrypt_offline_warning": "ウォレットファイルを持つ人は誰でも、オフラインでパスフレーズを試せます。先にシードフレーズをバックアップし、長いパスフレーズを使ってください。",
|
||||
"lite_encrypt_wallet": "ウォレットを暗号化",
|
||||
"lite_encryption_removed": "暗号化を解除しました",
|
||||
"lite_enter_all_seed_words": "復元するには24個のシードワードをすべて入力してください(現在 %d 個)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||
"tt_language": "ウォレット UI のインターフェース言語",
|
||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
||||
"tt_lite_confirm_pass": "新しいパスフレーズをもう一度入力してください。一致し、8 文字以上である必要があります",
|
||||
"tt_lite_copy": "表示された秘密情報をクリップボードにコピーします",
|
||||
"tt_lite_decrypt_pass": "ウォレットの暗号化を解除するためにパスフレーズを入力します",
|
||||
"tt_lite_encrypt": "上のパスフレーズでウォレットを暗号化します。すぐにロックされ、解除にはパスフレーズが必要です",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||
"lite_birthday_label": "생일 블록",
|
||||
"lite_confirm_label": "확인",
|
||||
"lite_console_backend_commands": "백엔드 명령:",
|
||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||
"lite_copy": "복사",
|
||||
"lite_could_not_start": "작업을 시작할 수 없습니다",
|
||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||
"lite_encrypt_offline_warning": "지갑 파일을 가진 사람은 누구나 오프라인으로 암호를 시도할 수 있습니다. 먼저 시드 문구를 백업하고 긴 암호를 사용하세요.",
|
||||
"lite_encrypt_wallet": "지갑 암호화",
|
||||
"lite_encryption_removed": "암호화가 제거되었습니다",
|
||||
"lite_enter_all_seed_words": "복구하려면 24개의 시드 단어를 모두 입력하세요 (현재 %d개)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||
"tt_language": "지갑 UI 인터페이스 언어",
|
||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
||||
"tt_lite_confirm_pass": "새 암호를 한 번 더 입력하세요. 일치해야 하며 8자 이상이어야 합니다",
|
||||
"tt_lite_copy": "표시된 비밀을 클립보드에 복사합니다",
|
||||
"tt_lite_decrypt_pass": "지갑에서 암호화를 제거하려면 암호를 입력하세요",
|
||||
"tt_lite_encrypt": "위 암호로 지갑을 암호화합니다; 즉시 잠기며 잠금 해제하려면 암호가 필요합니다",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "Aniversário: %llu (faça o backup disto também)",
|
||||
"lite_birthday_hint": "Altura do bloco a partir da qual começar a escanear. Deixe 0 se desconhecida (escaneamento completo mais lento).",
|
||||
"lite_birthday_label": "Data de nascimento",
|
||||
"lite_confirm_label": "Confirmar",
|
||||
"lite_console_backend_commands": "Comandos do backend:",
|
||||
"lite_console_help_passthrough": "Qualquer outra entrada é executada como um comando de console da carteira leve.",
|
||||
"lite_copy": "Copiar",
|
||||
"lite_could_not_start": "Não foi possível iniciar a operação",
|
||||
"lite_could_not_write": "Não foi possível gravar ",
|
||||
"lite_encrypt_offline_warning": "Qualquer pessoa com o arquivo da carteira pode testar frases-passe offline. Faça primeiro o backup da sua frase de recuperação e use uma frase-passe longa.",
|
||||
"lite_encrypt_wallet": "Criptografar carteira",
|
||||
"lite_encryption_removed": "Criptografia removida",
|
||||
"lite_enter_all_seed_words": "Informe todas as 24 palavras-semente para restaurar (obtidas %d)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "O daemon será parado ao executar o assistente de configuração",
|
||||
"tt_language": "Idioma da interface da carteira",
|
||||
"tt_layout_hotkey": "Atalho: teclas de seta esquerda/direita para alternar layouts de Saldo",
|
||||
"tt_lite_confirm_pass": "Digite a nova frase-passe novamente; ela deve coincidir e ter pelo menos 8 caracteres",
|
||||
"tt_lite_copy": "Copiar o segredo revelado para a área de transferência",
|
||||
"tt_lite_decrypt_pass": "Digite sua frase-senha para remover a criptografia da carteira",
|
||||
"tt_lite_encrypt": "Criptografar a carteira com a frase-senha acima; ela é bloqueada imediatamente e exige a frase-senha para desbloquear",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "Дата рождения: %llu (сохраните её тоже)",
|
||||
"lite_birthday_hint": "Высота блока, с которой начинать сканирование. Оставьте 0, если неизвестно (медленное полное сканирование).",
|
||||
"lite_birthday_label": "Дата рождения",
|
||||
"lite_confirm_label": "Повтор",
|
||||
"lite_console_backend_commands": "Команды бэкенда:",
|
||||
"lite_console_help_passthrough": "Любой другой ввод выполняется как команда консоли лайт-кошелька.",
|
||||
"lite_copy": "Копировать",
|
||||
"lite_could_not_start": "Не удалось запустить операцию",
|
||||
"lite_could_not_write": "Не удалось записать ",
|
||||
"lite_encrypt_offline_warning": "Любой, у кого есть файл кошелька, может подбирать парольные фразы офлайн. Сначала сохраните сид-фразу и используйте длинную парольную фразу.",
|
||||
"lite_encrypt_wallet": "Зашифровать кошелёк",
|
||||
"lite_encryption_removed": "Шифрование удалено",
|
||||
"lite_enter_all_seed_words": "Введите все 24 слова seed-фразы для восстановления (введено %d)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "Демон будет остановлен при запуске мастера настройки",
|
||||
"tt_language": "Язык интерфейса кошелька",
|
||||
"tt_layout_hotkey": "Горячая клавиша: стрелки влево/вправо для переключения раскладок Баланса",
|
||||
"tt_lite_confirm_pass": "Введите новую парольную фразу ещё раз; она должна совпадать и содержать не менее 8 символов",
|
||||
"tt_lite_copy": "Скопировать показанный секрет в буфер обмена",
|
||||
"tt_lite_decrypt_pass": "Введите пароль, чтобы снять шифрование с кошелька",
|
||||
"tt_lite_encrypt": "Зашифровать кошелёк паролем выше; он блокируется сразу же и требует пароль для разблокировки",
|
||||
|
||||
@@ -1008,11 +1008,13 @@
|
||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||
"lite_birthday_label": "诞生区块",
|
||||
"lite_confirm_label": "确认",
|
||||
"lite_console_backend_commands": "后端命令:",
|
||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||
"lite_copy": "复制",
|
||||
"lite_could_not_start": "无法启动该操作",
|
||||
"lite_could_not_write": "无法写入 ",
|
||||
"lite_encrypt_offline_warning": "任何拿到钱包文件的人都可以离线尝试密码。请先备份助记词,并使用较长的密码短语。",
|
||||
"lite_encrypt_wallet": "加密钱包",
|
||||
"lite_encryption_removed": "已移除加密",
|
||||
"lite_enter_all_seed_words": "请输入全部 24 个助记词以恢复(已输入 %d 个)",
|
||||
@@ -2059,6 +2061,7 @@
|
||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||
"tt_language": "钱包界面语言",
|
||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
||||
"tt_lite_confirm_pass": "再次输入新的密码短语;两次输入必须一致,且至少 8 个字符",
|
||||
"tt_lite_copy": "将显示的机密复制到剪贴板",
|
||||
"tt_lite_decrypt_pass": "输入你的密码以移除钱包的加密",
|
||||
"tt_lite_encrypt": "用上方的密码加密钱包;加密后立即锁定,需要该密码才能解锁",
|
||||
|
||||
@@ -120,6 +120,8 @@ struct SettingsPageState {
|
||||
int lite_lifecycle_operation = 0;
|
||||
char lite_wallet_path[256] = "";
|
||||
char lite_lifecycle_passphrase[128] = "";
|
||||
// Confirmation of a NEW Create/Restore passphrase (SECRET: wiped with the passphrase). Open never uses it.
|
||||
char lite_lifecycle_passphrase_confirm[128] = "";
|
||||
char lite_restore_seed[512] = "";
|
||||
int lite_restore_birthday = 0;
|
||||
int lite_restore_account = 0;
|
||||
@@ -140,7 +142,9 @@ struct SettingsPageState {
|
||||
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.
|
||||
// lite_enc_confirm confirms a NEW passphrase for Encrypt only; Unlock never uses it.
|
||||
char lite_enc_pass[128] = "";
|
||||
char lite_enc_confirm[128] = "";
|
||||
char lite_dec_pass[128] = "";
|
||||
std::string lite_encryption_status;
|
||||
bool mine_when_idle = false;
|
||||
@@ -245,6 +249,22 @@ static int liteSeedWordCount(const char* s) {
|
||||
dragonx::util::normalizeSeedPhrase(s ? std::string(s) : std::string()));
|
||||
}
|
||||
|
||||
// Why a NEW passphrase is not accepted (empty = nothing to report), with the full-node wizard's
|
||||
// wording. While typing, an empty passphrase or confirmation is not reported yet; on submit
|
||||
// (`submitted`), a missing confirmation counts as a mismatch.
|
||||
static std::string liteNewPassphraseIssue(const char* pass, const char* confirm, bool submitted) {
|
||||
const size_t len = std::strlen(pass);
|
||||
if (len < wallet::kLiteMinNewPassphraseLength && (len > 0 || submitted)) {
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), TR("wiz_pass_too_short"), len);
|
||||
return buf;
|
||||
}
|
||||
if (len >= wallet::kLiteMinNewPassphraseLength && (confirm[0] != '\0' || submitted) &&
|
||||
std::strcmp(pass, confirm) != 0)
|
||||
return TR("wiz_pass_mismatch");
|
||||
return std::string();
|
||||
}
|
||||
|
||||
static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
|
||||
switch (s_settingsState.lite_lifecycle_operation) {
|
||||
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
|
||||
@@ -298,6 +318,8 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
||||
~LiteSecretScrubber() {
|
||||
sodium_memzero(s_settingsState.lite_lifecycle_passphrase,
|
||||
sizeof(s_settingsState.lite_lifecycle_passphrase));
|
||||
sodium_memzero(s_settingsState.lite_lifecycle_passphrase_confirm,
|
||||
sizeof(s_settingsState.lite_lifecycle_passphrase_confirm));
|
||||
sodium_memzero(s_settingsState.lite_restore_seed,
|
||||
sizeof(s_settingsState.lite_restore_seed));
|
||||
wallet::secureWipeLiteSecret(in.request.createRequest.passphrase);
|
||||
@@ -333,6 +355,19 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
||||
}
|
||||
}
|
||||
|
||||
// A NEW passphrase (Create/Restore with one entered) must be 8+ characters and confirmed, so a
|
||||
// typo can't lock the user out. Open is never checked: its passphrase unlocks an existing
|
||||
// wallet, which may predate the rule. (The scrubber above still wipes both fields here.)
|
||||
if (!wallet::liteLifecyclePassphraseAcceptable(input.request.operation,
|
||||
s_settingsState.lite_lifecycle_passphrase,
|
||||
s_settingsState.lite_lifecycle_passphrase_confirm)) {
|
||||
s_settingsState.lite_lifecycle_status =
|
||||
liteNewPassphraseIssue(s_settingsState.lite_lifecycle_passphrase,
|
||||
s_settingsState.lite_lifecycle_passphrase_confirm, /*submitted=*/true);
|
||||
s_settingsState.lite_lifecycle_summary.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// When a linked lite backend is present, execute the operation for real through the App-owned
|
||||
// controller — ASYNCHRONOUSLY, with server failover, so an unreachable/flaky server never
|
||||
// freezes the UI. The request (with its secrets) is moved into the controller; the page polls
|
||||
@@ -1749,6 +1784,35 @@ void RenderSettingsPage(App* app) {
|
||||
ImGuiInputTextFlags_Password);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_pass"));
|
||||
|
||||
// Create/Restore with a passphrase sets a NEW one: confirm it (8+ characters).
|
||||
// Open unlocks an existing wallet and is never checked.
|
||||
const bool liteNewPassphrase = s_settingsState.lite_lifecycle_operation != 1 &&
|
||||
s_settingsState.lite_lifecycle_passphrase[0] != '\0';
|
||||
if (liteNewPassphrase) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("lite_confirm_label"));
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(liteInputW);
|
||||
ImGui::InputText("##LiteLifecyclePassphraseConfirm",
|
||||
s_settingsState.lite_lifecycle_passphrase_confirm,
|
||||
sizeof(s_settingsState.lite_lifecycle_passphrase_confirm),
|
||||
ImGuiInputTextFlags_Password);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_confirm_pass"));
|
||||
const std::string issue = liteNewPassphraseIssue(
|
||||
s_settingsState.lite_lifecycle_passphrase,
|
||||
s_settingsState.lite_lifecycle_passphrase_confirm, /*submitted=*/false);
|
||||
if (!issue.empty()) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
Type().textColored(TypeStyle::Caption, Error(), issue.c_str());
|
||||
}
|
||||
} else {
|
||||
// Hidden (Open, or the passphrase was cleared): always wipe the whole buffer; an
|
||||
// emptied field can still hold an old passphrase past its first NUL.
|
||||
sodium_memzero(s_settingsState.lite_lifecycle_passphrase_confirm,
|
||||
sizeof(s_settingsState.lite_lifecycle_passphrase_confirm));
|
||||
}
|
||||
|
||||
// Poll a completed async create/open/restore (driven by the App-owned
|
||||
// controller; finalized on the main thread by App::update's
|
||||
// pumpLifecycleResult()). While in flight the button is disabled.
|
||||
@@ -1773,13 +1837,18 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
|
||||
if (liteLifecycleBusy) ImGui::BeginDisabled();
|
||||
// Also unavailable while a NEW passphrase is unconfirmed or too short (the hint
|
||||
// above says why), so a click never wipes a typed seed over a passphrase typo.
|
||||
const bool liteLifecycleBlocked = s_settingsState.lite_lifecycle_pending ||
|
||||
!wallet::liteLifecyclePassphraseAcceptable(liteLifecycleOperationFromPageState(),
|
||||
s_settingsState.lite_lifecycle_passphrase,
|
||||
s_settingsState.lite_lifecycle_passphrase_confirm);
|
||||
if (liteLifecycleBlocked) ImGui::BeginDisabled();
|
||||
if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
evaluateLiteLifecycleRequestFromPageState(app);
|
||||
}
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_lite_lifecycle_run"));
|
||||
if (liteLifecycleBusy) ImGui::EndDisabled();
|
||||
if (liteLifecycleBlocked) ImGui::EndDisabled();
|
||||
|
||||
if (!s_settingsState.lite_lifecycle_status.empty()) {
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
@@ -1949,8 +2018,30 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
|
||||
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt_pass"));
|
||||
// A NEW passphrase: confirm it and require 8+ characters (a typo here locks
|
||||
// the wallet at once). Unlock below never applies this rule.
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::BeginDisabled(liteScanning || app->liteWallet()->walletOpInProgress());
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("lite_confirm_label"));
|
||||
ImGui::SameLine(encLabelX);
|
||||
ImGui::SetNextItemWidth(liteInputW);
|
||||
ImGui::InputText("##LiteEncryptConfirm", s_settingsState.lite_enc_confirm,
|
||||
sizeof(s_settingsState.lite_enc_confirm), ImGuiInputTextFlags_Password);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_confirm_pass"));
|
||||
const std::string encIssue = liteNewPassphraseIssue(
|
||||
s_settingsState.lite_enc_pass, s_settingsState.lite_enc_confirm, /*submitted=*/false);
|
||||
if (!encIssue.empty()) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
Type().textColored(TypeStyle::Caption, Error(), encIssue.c_str());
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::PushTextWrapPos((leftX + leftColW) - ImGui::GetWindowPos().x);
|
||||
Type().textColored(TypeStyle::Caption, Warning(), TR("lite_encrypt_offline_warning"));
|
||||
ImGui::PopTextWrapPos();
|
||||
const bool encPassOk = wallet::liteEncryptPassphraseValid(s_settingsState.lite_enc_pass,
|
||||
s_settingsState.lite_enc_confirm);
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::BeginDisabled(liteScanning || app->liteWallet()->walletOpInProgress() || !encPassOk);
|
||||
if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
s_settingsState.lite_encryption_status = TR("lite_working");
|
||||
app->liteWallet()->encryptWalletAsync(s_settingsState.lite_enc_pass,
|
||||
@@ -1959,11 +2050,15 @@ void RenderSettingsPage(App* app) {
|
||||
r.ok ? std::string(TR("lite_wallet_encrypted")) : r.error;
|
||||
});
|
||||
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
||||
sodium_memzero(s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
material::Tooltip("%s", liteScanning ? TR("send_tooltip_syncing") : TR("tt_lite_encrypt"));
|
||||
} else {
|
||||
// Encrypted: the Encrypt confirmation is hidden; wipe the whole buffer (an emptied field
|
||||
// can still hold an old passphrase past its first NUL).
|
||||
sodium_memzero(s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm));
|
||||
if (wstate.isLocked()) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -2036,11 +2131,15 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc"));
|
||||
} else if (!s_settingsState.lite_export_secret.empty()) {
|
||||
// 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();
|
||||
} else {
|
||||
// Wallet closed: don't leave a revealed backup/secret or a typed passphrase in memory.
|
||||
if (!s_settingsState.lite_export_secret.empty()) {
|
||||
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||
s_settingsState.lite_export_label.clear();
|
||||
}
|
||||
// Unconditional: an emptied field can still hold the old tail past its first NUL.
|
||||
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
||||
sodium_memzero(s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm));
|
||||
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
||||
}
|
||||
} // lite WALLET block
|
||||
|
||||
@@ -826,6 +826,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["tt_lite_import_key"] = "Paste a private spending or viewing key to import; its history appears after the next sync";
|
||||
strings_["tt_lite_import_key_btn"] = "Import the entered private key into this wallet; funds and history appear after the next sync";
|
||||
strings_["tt_lite_encrypt_pass"] = "Passphrase to encrypt the wallet with. If lost, the wallet cannot be unlocked or recovered";
|
||||
strings_["tt_lite_confirm_pass"] = "Type the new passphrase again; it must match and be at least 8 characters long";
|
||||
strings_["tt_lite_encrypt"] = "Encrypt the wallet with the passphrase above; it locks immediately and requires the passphrase to unlock";
|
||||
strings_["tt_lite_unlock_pass"] = "Enter your passphrase to unlock the encrypted wallet";
|
||||
strings_["tt_lite_unlock"] = "Unlock the encrypted wallet using the passphrase above";
|
||||
@@ -943,6 +944,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["lite_account_label"] = "Account";
|
||||
strings_["lite_overwrite"] = "Overwrite";
|
||||
strings_["lite_passphrase_label"] = "Passphrase";
|
||||
strings_["lite_confirm_label"] = "Confirm";
|
||||
strings_["lite_validate"] = "Validate";
|
||||
strings_["lite_working"] = "Working…";
|
||||
strings_["lite_enter_wallet_path"] = "Enter a wallet path";
|
||||
@@ -967,6 +969,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["lite_key_import_failed"] = "Couldn't import the key \xE2\x80\x94 check it, unlock the wallet (if encrypted) and try again.";
|
||||
strings_["lite_security"] = "Security";
|
||||
strings_["lite_encrypt_wallet"] = "Encrypt wallet";
|
||||
strings_["lite_encrypt_offline_warning"] = "Anyone with the wallet file can try passphrases offline. Back up your seed phrase first and use a long passphrase.";
|
||||
strings_["lite_wallet_encrypted"] = "Wallet encrypted";
|
||||
strings_["lite_unlock"] = "Unlock";
|
||||
strings_["lite_wallet_unlocked"] = "Wallet unlocked";
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
#include "lite_wallet_lifecycle_ui_adapter.h"
|
||||
|
||||
// The lite-wallet lifecycle UI adapter is now header-only: it declares the live
|
||||
// LiteWalletLifecycleUiExecutionInput DTO (and its request-intent / prerequisites
|
||||
// sub-structs) that Settings drives the App-owned controller with. The former
|
||||
// dry-run "readiness" executor and its status/issue reporting were removed as dead
|
||||
// scaffolding, leaving no out-of-line definitions in this translation unit.
|
||||
// The lite-wallet lifecycle UI adapter declares the live LiteWalletLifecycleUiExecutionInput DTO
|
||||
// (and its request-intent / prerequisites sub-structs) that Settings drives the App-owned
|
||||
// controller with. The former dry-run "readiness" executor and its status/issue reporting were
|
||||
// removed as dead scaffolding. The only out-of-line definitions here are the pure passphrase rules
|
||||
// Settings applies to NEW passphrases.
|
||||
|
||||
namespace dragonx {
|
||||
namespace wallet {
|
||||
|
||||
bool liteEncryptPassphraseValid(std::string_view pass, std::string_view confirm)
|
||||
{
|
||||
return pass.size() >= kLiteMinNewPassphraseLength && pass == confirm;
|
||||
}
|
||||
|
||||
bool liteLifecyclePassphraseAcceptable(LiteWalletLifecycleOperation operation,
|
||||
std::string_view pass, std::string_view confirm)
|
||||
{
|
||||
if (operation == LiteWalletLifecycleOperation::OpenExisting) return true;
|
||||
if (pass.empty()) return true;
|
||||
return liteEncryptPassphraseValid(pass, confirm);
|
||||
}
|
||||
|
||||
} // namespace wallet
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -6,9 +6,27 @@
|
||||
|
||||
#include "../config/settings.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace dragonx {
|
||||
namespace wallet {
|
||||
|
||||
// Minimum length of a NEW Lite wallet passphrase, in bytes (the full-node encrypt dialog's rule).
|
||||
inline constexpr std::size_t kLiteMinNewPassphraseLength = 8;
|
||||
|
||||
// A NEW passphrase (Settings "Encrypt wallet", or Create/Restore with a passphrase) must be at
|
||||
// least kLiteMinNewPassphraseLength long and equal its confirmation. Pure, and it takes views so
|
||||
// the page's secret buffers are never copied. Never apply it to opening or unlocking an existing
|
||||
// wallet: that passphrase may predate the rule.
|
||||
bool liteEncryptPassphraseValid(std::string_view pass, std::string_view confirm);
|
||||
|
||||
// The passphrase gate for a Settings create/open/restore request. OpenExisting is never checked,
|
||||
// because its passphrase unlocks an existing wallet. CreateNew and RestoreFromSeed are checked only
|
||||
// when a passphrase was entered; an empty one creates an unencrypted wallet.
|
||||
bool liteLifecyclePassphraseAcceptable(LiteWalletLifecycleOperation operation,
|
||||
std::string_view pass, std::string_view confirm);
|
||||
|
||||
struct LiteWalletLifecycleUiPrerequisites {
|
||||
bool selectedServerDisplayReady = false;
|
||||
bool lifecycleUiOwnerReady = false;
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
#include "wallet/lite_owned_string.h"
|
||||
#include "wallet/lite_rollout_policy.h"
|
||||
#include "wallet/lite_wallet_controller.h"
|
||||
#include "wallet/lite_wallet_lifecycle_ui_adapter.h"
|
||||
#include "wallet/lite_wallet_gateway.h"
|
||||
#include "wallet/lite_wallet_state_mapper.h"
|
||||
#include "config/settings.h"
|
||||
@@ -8783,6 +8784,90 @@ void testLiteWalletControllerAsyncAppliesPassphrase()
|
||||
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
|
||||
}
|
||||
|
||||
// C2 (s1-D8, X7): a NEW Lite passphrase (Settings "Encrypt wallet", or Create/Restore with one entered)
|
||||
// must be 8+ characters and confirmed. Opening and unlocking an existing wallet never apply the rule, so
|
||||
// a wallet encrypted earlier with a short passphrase still opens and unlocks.
|
||||
void testLiteNewPassphraseRules()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
using Op = LiteWalletLifecycleOperation;
|
||||
|
||||
EXPECT_EQ(kLiteMinNewPassphraseLength, static_cast<std::size_t>(8));
|
||||
|
||||
// The validator: empty, short or mismatched is rejected; 8+ and confirmed is accepted.
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("", ""));
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("a", "a"));
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("1234567", "1234567")); // 7 < 8
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("", "12345678"));
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("12345678", "")); // not confirmed
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("12345678", "12345679")); // mismatch
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("12345678", "12345678 ")); // a trailing space is a mismatch
|
||||
EXPECT_TRUE(liteEncryptPassphraseValid("12345678", "12345678")); // exactly 8
|
||||
EXPECT_TRUE(liteEncryptPassphraseValid("correct horse battery staple", "correct horse battery staple"));
|
||||
{
|
||||
// The page passes its fixed char buffers straight in (no copy of the secret).
|
||||
char pass[128] = "hunter2hunter2";
|
||||
char confirm[128] = "hunter2hunter2";
|
||||
EXPECT_TRUE(liteEncryptPassphraseValid(pass, confirm));
|
||||
confirm[0] = 'H';
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid(pass, confirm));
|
||||
}
|
||||
|
||||
// The Settings request gate. OpenExisting is never validated: a short or unconfirmed passphrase
|
||||
// for an existing wallet passes.
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, "abc", ""));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, "hunter2", "different"));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, "", ""));
|
||||
// Create/Restore without a passphrase make an unencrypted wallet: nothing to confirm.
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::CreateNew, "", ""));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::RestoreFromSeed, "", ""));
|
||||
// With a passphrase they set a NEW one: 8+ characters and confirmed.
|
||||
for (const Op op : {Op::CreateNew, Op::RestoreFromSeed}) {
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "abc", "abc"));
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "1234567", "1234567"));
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "12345678", ""));
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "12345678", "1234567X"));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(op, "12345678", "12345678"));
|
||||
}
|
||||
|
||||
// The controller's open and unlock paths take an existing short passphrase as is: an encrypted,
|
||||
// locked wallet opens and unlocks with "abc" (as the Settings Open request sends it), and unlocks
|
||||
// with it again after a lock.
|
||||
{
|
||||
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||
LiteConnectionSettings conn;
|
||||
conn.chainName = "main";
|
||||
conn.servers = { LiteServerEndpoint{"https://good.example", "Good", true} };
|
||||
conn.selectionMode = LiteServerSelectionMode::Sticky;
|
||||
conn.stickyServerUrl = "https://good.example";
|
||||
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
dragonx::test::g_liteFakeWalletExists = true;
|
||||
dragonx::test::g_liteFakeEncrypted = true;
|
||||
dragonx::test::g_liteFakeLocked = true;
|
||||
LiteWalletController controller(liteCaps, conn,
|
||||
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletOpenRequest req;
|
||||
req.passphrase = "abc";
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, req.passphrase, ""));
|
||||
EXPECT_TRUE(controller.beginOpenWalletAsync(req));
|
||||
for (int i = 0; i < 400 && controller.lifecycleRequestInProgress(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
controller.pumpLifecycleResult();
|
||||
EXPECT_TRUE(controller.walletOpen());
|
||||
EXPECT_TRUE(controller.encryptionStatus().encrypted);
|
||||
EXPECT_FALSE(controller.encryptionStatus().locked); // the short passphrase unlocked it
|
||||
EXPECT_TRUE(controller.lockWallet());
|
||||
EXPECT_TRUE(controller.encryptionStatus().locked);
|
||||
EXPECT_TRUE(controller.unlockWallet("abc"));
|
||||
EXPECT_FALSE(controller.encryptionStatus().locked);
|
||||
}
|
||||
|
||||
dragonx::test::g_liteFakeWalletExists = false;
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
}
|
||||
|
||||
// WalletState::tx_revision (the History / Mining caches key on it): sameTransactionInfo covers every
|
||||
// TransactionInfo field and compares `amount` bit-exactly; setTransactionsIfChanged bumps only on a real change
|
||||
// and otherwise keeps the old list.
|
||||
@@ -15713,6 +15798,7 @@ int main()
|
||||
testLiteWalletControllerEncryption();
|
||||
testLiteWalletControllerCreateEncryptsWithPassphrase();
|
||||
testLiteWalletControllerAsyncAppliesPassphrase();
|
||||
testLiteNewPassphraseRules();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testSameTransactionList();
|
||||
|
||||
Reference in New Issue
Block a user