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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "今すぐロック",
|
||||
|
||||
@@ -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": "지금 잠그기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Заблокировать сейчас",
|
||||
|
||||
@@ -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": "立即锁定",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user