fix(lite): keep a rejected import key out of the log and Settings

The backend's error for a key it doesn't recognize repeats the key
("'<key>' was not recognized as either a spending key or a viewing
key"). Import passed that text on as its error, so a mistyped private
key was written to the lite log (shown in the Console tab) and to the
Settings status line. A failed import now carries a fixed reason
(unrecognized format, wallet locked, key already exists, could not be
decoded, or rejected), which is all the log records. Settings shows
"Couldn't import the key — check it, unlock the wallet (if encrypted)
and try again." instead, translated in all eight languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent 7c24400c83
commit bb84243114
13 changed files with 69 additions and 6 deletions
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "Ausblenden & löschen",
"lite_import": "Importieren",
"lite_import_key_label": "Schlüssel importieren",
"lite_key_import_failed": "Schlüssel konnte nicht importiert werden — prüfen Sie ihn, entsperren Sie die Wallet (falls verschlüsselt) und versuchen Sie es erneut.",
"lite_key_imported": "Schlüssel importiert — führen Sie eine Synchronisierung durch, um den Verlauf zu scannen",
"lite_lock_failed": "Sperren fehlgeschlagen",
"lite_lock_now": "Jetzt sperren",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "Ocultar y borrar",
"lite_import": "Importar",
"lite_import_key_label": "Importar clave",
"lite_key_import_failed": "No se pudo importar la clave: compruébala, desbloquea el monedero (si está cifrado) e inténtalo de nuevo.",
"lite_key_imported": "Clave importada: ejecuta una sincronización para escanear su historial",
"lite_lock_failed": "Error al bloquear",
"lite_lock_now": "Bloquear ahora",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "Masquer et effacer",
"lite_import": "Importer",
"lite_import_key_label": "Importer une clé",
"lite_key_import_failed": "Impossible d'importer la clé — vérifiez-la, déverrouillez le portefeuille (s'il est chiffré) et réessayez.",
"lite_key_imported": "Clé importée — lancez une synchronisation pour analyser son historique",
"lite_lock_failed": "Échec du verrouillage",
"lite_lock_now": "Verrouiller maintenant",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "非表示にして消去",
"lite_import": "インポート",
"lite_import_key_label": "鍵をインポート",
"lite_key_import_failed": "鍵をインポートできませんでした。鍵を確認し、ウォレットのロックを解除して(暗号化されている場合)再試行してください。",
"lite_key_imported": "鍵をインポートしました — 履歴をスキャンするには同期を実行してください",
"lite_lock_failed": "ロックに失敗しました",
"lite_lock_now": "今すぐロック",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "숨기고 삭제",
"lite_import": "가져오기",
"lite_import_key_label": "키 가져오기",
"lite_key_import_failed": "키를 가져오지 못했습니다. 키를 확인하고 지갑 잠금을 해제한 후(암호화된 경우) 다시 시도하세요.",
"lite_key_imported": "키를 가져왔습니다 — 동기화를 실행하여 기록을 스캔하세요",
"lite_lock_failed": "잠금 실패",
"lite_lock_now": "지금 잠그기",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "Ocultar e apagar",
"lite_import": "Importar",
"lite_import_key_label": "Importar chave",
"lite_key_import_failed": "Não foi possível importar a chave — verifique-a, desbloqueie a carteira (se estiver criptografada) e tente novamente.",
"lite_key_imported": "Chave importada — execute uma sincronização para escanear seu histórico",
"lite_lock_failed": "Falha ao bloquear",
"lite_lock_now": "Bloquear agora",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "Скрыть и стереть",
"lite_import": "Импорт",
"lite_import_key_label": "Импортировать ключ",
"lite_key_import_failed": "Не удалось импортировать ключ — проверьте его, разблокируйте кошелёк (если зашифрован) и повторите попытку.",
"lite_key_imported": "Ключ импортирован — выполните синхронизацию, чтобы просканировать его историю",
"lite_lock_failed": "Не удалось заблокировать",
"lite_lock_now": "Заблокировать сейчас",
+1
View File
@@ -1020,6 +1020,7 @@
"lite_hide_wipe": "隐藏并清除",
"lite_import": "导入",
"lite_import_key_label": "导入密钥",
"lite_key_import_failed": "无法导入密钥——请检查密钥,解锁钱包(如已加密)后重试。",
"lite_key_imported": "密钥已导入——运行同步以扫描其历史记录",
"lite_lock_failed": "锁定失败",
"lite_lock_now": "立即锁定",
+2 -1
View File
@@ -1916,8 +1916,9 @@ void RenderSettingsPage(App* app) {
} else {
s_settingsState.lite_backup_status = TR("lite_working");
app->liteWallet()->importKeyAsync(liteKey, [](const wallet::LiteImportResult& r) {
// Generic text: the reason (non-secret) is in the lite log.
s_settingsState.lite_backup_status =
r.ok ? std::string(TR("lite_key_imported")) : r.error;
r.ok ? TR("lite_key_imported") : TR("lite_key_import_failed");
});
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
}
+1
View File
@@ -951,6 +951,7 @@ void I18n::loadBuiltinEnglish()
strings_["lite_import_key_label"] = "Import key";
strings_["lite_import"] = "Import";
strings_["lite_key_imported"] = "Key imported — run a sync to scan its history";
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_wallet_encrypted"] = "Wallet encrypted";
+16 -4
View File
@@ -163,7 +163,19 @@ LiteNewAddressResult newAddressCore(LiteClientBridge& bridge, bool shielded)
return out;
}
// `key` must be non-empty; it is wiped before returning.
// The backend's import errors can repeat the key itself (do_import_key: "'<key>' was not recognized as either a
// spending key or a viewing key ..."), so that text is never logged or shown: only a fixed, non-secret reason.
std::string liteImportFailureReason(const std::string& backendError)
{
const auto has = [&backendError](const char* s) { return backendError.find(s) != std::string::npos; };
if (has("was not recognized")) return "unrecognized key format";
if (has("locked")) return "wallet is locked";
if (has("already exists")) return "key already exists";
if (has("decode")) return "key could not be decoded";
return "key rejected by the backend";
}
// `key` must be non-empty; it is wiped before returning. A failure's error is a fixed reason, never backend text.
LiteImportResult importKeyCore(LiteClientBridge& bridge, std::string& key, const std::atomic<bool>* retired = nullptr)
{
// Transparent WIFs begin with U/5/K/L (TImportCommand); shielded keys begin with
@@ -180,13 +192,13 @@ LiteImportResult importKeyCore(LiteClientBridge& bridge, std::string& key, const
LiteImportResult r;
const auto result = bridge.execute(command, key);
if (!result.ok) { // do_import_* failures come back "Error:"-prefixed (bridge -> ok=false)
r.error = result.error.empty() ? "key import failed" : result.error;
r.error = liteImportFailureReason(result.error);
return r;
}
try {
const auto j = nlohmann::json::parse(result.value);
if (j.is_object() && j.contains("error")) {
r.error = extractJsonError(j);
r.error = liteImportFailureReason(extractJsonError(j));
return r;
}
} catch (...) {
@@ -207,7 +219,7 @@ LiteImportResult importKeyCore(LiteClientBridge& bridge, std::string& key, const
LiteImportResult alt = runImport(transparentFirst ? "import" : "timport");
if (alt.ok) out = alt;
}
if (!out.ok) liteLog("Key import failed: " + out.error); // error text only — never the key
if (!out.ok) liteLog("Key import failed: " + out.error); // a fixed reason: the backend's text can repeat the key
secureWipeLiteSecret(key); // wipe our copy after both attempts
return out;
}
+2 -1
View File
@@ -97,7 +97,8 @@ struct LiteBroadcastResult {
std::string error;
};
// importKey outcome. `detail` is the backend's (non-secret) confirmation payload.
// importKey outcome. `detail` is the backend's (non-secret) confirmation payload. `error` is a fixed, non-secret reason,
// never the backend's text (which can repeat the key).
struct LiteImportResult {
bool ok = false;
std::string detail;
+40
View File
@@ -8376,6 +8376,45 @@ void testLiteWalletControllerRetireStopsOpFollowUps()
dragonx::test::g_liteFakeLocked = false;
}
// The backend's import error for an unrecognized key repeats the key ("'<key>' was not recognized ..."): the controller
// must never put that text in its result (shown in Settings) or in the lite log, only a fixed, non-secret reason.
void testLiteWalletControllerImportErrorNeverRepeatsKey()
{
using namespace dragonx::wallet;
const auto caps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
const auto conn = defaultLiteConnectionSettings();
dragonx::test::resetLiteFakeCounters();
LiteWalletController c(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
EXPECT_TRUE(c.createWallet(LiteWalletCreateRequest{}).walletReady);
for (int i = 0; i < 400 && !c.syncComplete(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(5));
dragonx::test::g_liteFakeImportFails = true;
const auto logMentions = [](const std::string& needle) {
for (const auto& line : LiteDiagnostics::instance().snapshot())
if (line.find(needle) != std::string::npos) return true;
return false;
};
{ // synchronous: both attempts fail; neither the result nor the log carries the key
const auto r = c.importKey("secret-extended-key-typoMARKSYNC");
EXPECT_FALSE(r.ok);
EXPECT_EQ(r.error, std::string("unrecognized key format"));
EXPECT_TRUE(r.error.find("MARKSYNC") == std::string::npos);
EXPECT_FALSE(logMentions("MARKSYNC"));
EXPECT_TRUE(logMentions("Key import failed: unrecognized key format"));
}
{ // async (the Settings path)
int n = 0; LiteImportResult got; got.ok = true;
EXPECT_TRUE(c.importKeyAsync("Kx1typoMARKASYNC", [&](const LiteImportResult& r) { ++n; got = r; }));
EXPECT_TRUE(c.waitWalletOpsIdle(std::chrono::milliseconds(3000)));
c.pumpWalletOps();
EXPECT_EQ(n, 1);
EXPECT_FALSE(got.ok);
EXPECT_TRUE(got.error.find("MARKASYNC") == std::string::npos);
EXPECT_FALSE(logMentions("MARKASYNC"));
}
dragonx::test::g_liteFakeImportFails = false;
}
void testLiteWalletControllerEncryptsBeforeSyncStarts()
{
using namespace dragonx::wallet;
@@ -13597,6 +13636,7 @@ int main()
testLiteWalletControllerWalletOpsAsync();
testLiteWalletControllerRetire();
testLiteWalletControllerRetireStopsOpFollowUps();
testLiteWalletControllerImportErrorNeverRepeatsKey();
testLiteWalletControllerEncryptsBeforeSyncStarts();
testLiteWalletControllerWorkerBacksOffOnSyncReadFailure();
testLiteWalletControllerDestroyWithBlockedOp();