fix(security): only a real wrong passphrase counts toward unlock lockout
A failed wallet unlock was always treated as a wrong guess: the lockout counter was bumped and the back-off curve armed no matter why the RPC failed. A busy or warming-up node, a transport timeout, or a wallet that is not actually encrypted would all eat an attempt and could push a user into a lockout without a single mistyped passphrase. The failed call is now classified before it is acted on: - Only the daemon's "passphrase incorrect" error counts as a wrong guess and advances the lockout. - A warming-up or unreachable node, a reply timeout, or a full queue is reported as a transient busy-node condition with no attempt charged. - A "wallet is not encrypted" reply refreshes the local encryption state instead of penalising the user. - Any unknown error surfaces its message without touching the counter. The PIN path got the same treatment: when the vault decrypts correctly but the unlock RPC then fails for a non-credential reason, the attempt is no longer counted. Tests cover the classification of each failure kind. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -152,5 +152,30 @@ inline RpcFailureKind rpcFailureKindOf(const std::exception& e)
|
||||
return RpcFailureKind::Other;
|
||||
}
|
||||
|
||||
// What a failed walletpassphrase call means. Only WrongPassphrase (-14) may count toward the lockout;
|
||||
// a busy/warming-up node, a wallet that is not encrypted, or anything unknown is never a bad guess.
|
||||
enum class UnlockFailure { WrongPassphrase, NodeBusy, NotEncrypted, Other };
|
||||
|
||||
inline UnlockFailure classifyUnlockFailure(const std::exception& e)
|
||||
{
|
||||
if (const auto* r = dynamic_cast<const RpcError*>(&e)) {
|
||||
if (r->code == -14) return UnlockFailure::WrongPassphrase; // RPC_WALLET_PASSPHRASE_INCORRECT
|
||||
if (r->code == -15) return UnlockFailure::NotEncrypted; // RPC_WALLET_WRONG_ENC_STATE
|
||||
if (r->code == -28) return UnlockFailure::NodeBusy; // RPC_IN_WARMUP
|
||||
return UnlockFailure::Other;
|
||||
}
|
||||
switch (rpcFailureKindOf(e)) {
|
||||
case RpcFailureKind::ReplyTimeout:
|
||||
case RpcFailureKind::QueueFull:
|
||||
case RpcFailureKind::Deferred:
|
||||
case RpcFailureKind::Warmup:
|
||||
case RpcFailureKind::ConnectTimeout:
|
||||
case RpcFailureKind::NotListening:
|
||||
return UnlockFailure::NodeBusy;
|
||||
default:
|
||||
return UnlockFailure::Other;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
} // namespace dragonx
|
||||
|
||||
Reference in New Issue
Block a user