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:
2026-10-12 13:22:00 -05:00
co-authored by Claude Opus 5.5
parent e52c9e1b92
commit eac0993512
13 changed files with 262 additions and 13 deletions
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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": "上のパスフレーズでウォレットを暗号化します。すぐにロックされ、解除にはパスフレーズが必要です",
+3
View File
@@ -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": "위 암호로 지갑을 암호화합니다; 즉시 잠기며 잠금 해제하려면 암호가 필요합니다",
+3
View File
@@ -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",
+3
View File
@@ -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": "Зашифровать кошелёк паролем выше; он блокируется сразу же и требует пароль для разблокировки",
+3
View File
@@ -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": "用上方的密码加密钱包;加密后立即锁定,需要该密码才能解锁",
+107 -8
View File
@@ -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
+3
View File
@@ -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;
+86
View File
@@ -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();