fix(lite): lock/unlock parity — shared attempt-lockout, immediate unlock state, no dead PIN, surfaced encrypt failure
Lite-parity audit, FIX C (gating/validation/presentation only — no encrypt/unlock/send backend call changed): - Lite unlock went straight to the backend with no lockout. New App::unlockLiteWallet() runs the SAME escalating, persisted attempt-lockout as the full node (restoreLockoutState/applyUnlockFailure), wired into the Settings unlock, the send-time prompt and the lock screen. A wrong-passphrase failure counts; an already-unlocked/not-encrypted result does not. - state_.locked is now cleared immediately on a successful unlock (the ~2s status poll lagged, so the UI stayed "locked" after a correct passphrase). - PIN quick-unlock controls are hidden in lite Settings — lite has no PIN vault, so they were dead. - A create/restore whose passphrase encryption FAILS now raises a one-shot visible error (takeEncryptionFailureNotice -> lite_encrypt_failed_unencrypted) instead of only a log line; the wallet is left open but unencrypted. Fixed the flag on BOTH the lifecycle and the direct create/restore encrypt paths. - A background sync save is skipped while an encrypted wallet is unlocked (the backend save re-locks it), resuming once locked. Compile- and unit-verified (testLiteWalletControllerLockUnlockParity covers the save-deferral + encrypt-failure notice, run twice for the flaky guard); the lock-screen lockout/PIN-hiding UX itself is runtime/GUI-only and not exercised here — worth a manual lite GUI pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,21 @@ bool persistAfterBroadcast(LiteClientBridge& bridge)
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when the wallet is encrypted AND currently unlocked. The backend's `save` re-locks such a wallet (do_save),
|
||||
// so a background save must not run under a user who just unlocked. Fail-open: any error reads as "not unlocked" and
|
||||
// the save proceeds as it always did.
|
||||
bool liteWalletEncryptedAndUnlocked(LiteClientBridge& bridge)
|
||||
{
|
||||
const auto r = bridge.execute("encryptionstatus", "");
|
||||
if (!r.ok) return false;
|
||||
try {
|
||||
const auto j = nlohmann::json::parse(r.value);
|
||||
return j.is_object() && !j.contains("error") && j.value("encrypted", false) && !j.value("locked", false);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The detached sync/rescan thread. `firstCommand` is "sync" or "rescan"; every retry is "sync" (a failed rescan has
|
||||
// already cleared the wallet, so re-running it would throw away the partial progress). Partial progress is saved after
|
||||
// every attempt. Captures shared state only.
|
||||
@@ -129,7 +144,12 @@ void runLiteScanThread(const std::shared_ptr<LiteClientBridge>& bridge,
|
||||
op.error = call.error.empty() ? "sync failed" : call.error;
|
||||
}
|
||||
// The backend does NOT auto-save after a sync; persist what was scanned even when the attempt failed.
|
||||
if (!bridge->execute("save", "").ok) // W5-2
|
||||
// Not while the user has the wallet unlocked: the backend save re-locks an encrypted+unlocked wallet, which
|
||||
// would yank the unlock out from under a spend/unlock in progress. The progress is saved by the next scan
|
||||
// once locked (and by the shutdown save); a crash in between only costs a re-scan from the checkpoint.
|
||||
if (liteWalletEncryptedAndUnlocked(*bridge))
|
||||
liteLog("sync save deferred: wallet is unlocked (a save would re-lock it)");
|
||||
else if (!bridge->execute("save", "").ok) // W5-2
|
||||
liteLog("save failed after sync — the next launch will re-scan from the checkpoint");
|
||||
if (op.ok) {
|
||||
scan->setError(std::string());
|
||||
@@ -938,9 +958,11 @@ void LiteWalletController::applyPendingLifecycleSecurity()
|
||||
if (op == PendingSecurityOp::Encrypt) {
|
||||
// encryptWallet() takes its own copy and wipes it; the backend encrypts + locks + saves.
|
||||
const auto enc = encryptWallet(pendingLifecyclePassphrase_);
|
||||
if (!enc.ok)
|
||||
if (!enc.ok) {
|
||||
lifecycleEncryptFailed_ = true; // surfaced to the user by the App (takeEncryptionFailureNotice)
|
||||
liteLog("wallet created/restored but ENCRYPTION FAILED — wallet is NOT encrypted: " +
|
||||
enc.error);
|
||||
}
|
||||
} else if (op == PendingSecurityOp::Unlock) {
|
||||
if (!unlockWallet(pendingLifecyclePassphrase_))
|
||||
liteLog("wallet opened but unlock failed (wrong passphrase?)");
|
||||
@@ -1597,6 +1619,13 @@ bool LiteWalletController::unlockWallet(std::string passphrase)
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool LiteWalletController::takeEncryptionFailureNotice()
|
||||
{
|
||||
const bool failed = lifecycleEncryptFailed_;
|
||||
lifecycleEncryptFailed_ = false;
|
||||
return failed;
|
||||
}
|
||||
|
||||
bool LiteWalletController::lockWallet()
|
||||
{
|
||||
if (!walletOpen_.load() || !bridge_) return false;
|
||||
@@ -1767,7 +1796,10 @@ LiteWalletLifecycleResult LiteWalletController::createWallet(LiteWalletCreateReq
|
||||
// copy and wipes it.
|
||||
if (walletOpen_.load() && !request.passphrase.empty()) {
|
||||
const auto enc = encryptWallet(request.passphrase);
|
||||
if (!enc.ok) liteLog("wallet created but encryption failed: " + enc.error);
|
||||
if (!enc.ok) {
|
||||
lifecycleEncryptFailed_ = true; // the wallet is open but NOT encrypted; App surfaces this
|
||||
liteLog("wallet created but encryption failed: " + enc.error);
|
||||
}
|
||||
}
|
||||
secureWipeLiteSecret(request.passphrase);
|
||||
return result;
|
||||
@@ -1797,7 +1829,10 @@ LiteWalletLifecycleResult LiteWalletController::restoreWallet(LiteWalletRestoreR
|
||||
// If the user supplied a passphrase, encrypt the restored wallet with it now that it's open (W5-3).
|
||||
if (walletOpen_.load() && !request.passphrase.empty()) {
|
||||
const auto enc = encryptWallet(request.passphrase);
|
||||
if (!enc.ok) liteLog("wallet restored but encryption failed: " + enc.error);
|
||||
if (!enc.ok) {
|
||||
lifecycleEncryptFailed_ = true; // the wallet is open but NOT encrypted; App surfaces this
|
||||
liteLog("wallet restored but encryption failed: " + enc.error);
|
||||
}
|
||||
}
|
||||
secureWipeLiteSecret(request.seedPhrase);
|
||||
secureWipeLiteSecret(request.passphrase);
|
||||
|
||||
Reference in New Issue
Block a user