fix(security): don't penalise a correct PIN after a passphrase change
A passphrase change left the PIN vault sealing the old passphrase. The next PIN unlock opened the vault, walletpassphrase answered RPC -14, and the generic catch ran the lockout curve on a correct PIN, while Settings kept showing the PIN as active. - Passphrase change: when the active wallet's vault exists at completion (same vault path as at dispatch, passphrase actually changed), the vault is marked out of date for the session and kept on disk: PIN entry is no longer offered, a warning toast says so, and Settings shows "PIN out of date" with a tooltip on how to fix it. The Change Passphrase dialog notes beforehand that a PIN will go out of date. - PIN unlock: RPC -14 after the vault opened is treated as "PIN out of date": passphrase entry, a message, no attempt counted, no lockout, and the vault is not deleted (-14 is only the daemon's answer; the vault may hold the only copy of a passphrase that opens an older backup). After a restart Settings shows the PIN as active until the first PIN attempt. - A malformed vault (zero-filled by a removeVault() whose unlink failed, truncated, unknown version) is treated as missing: passphrase entry, "PIN file unreadable", no penalty. New SecureVault::isWellFormed(). - Race guards: Set/Change/Remove PIN are disabled while a passphrase change runs, and Change Passphrase while a PIN operation runs. - Setting or removing the PIN, or switching wallets, clears the mark; setting a PIN offers PIN entry again. Change PIN keeps the mark (the vault still seals the same passphrase). - Pure decisions in data/pin_vault_policy.h, unit-tested. New strings in all 8 languages. No vault, wallet or settings format change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Pure decisions for the PIN quick-unlock vault (C87), kept free of App/UI/RPC state so they are
|
||||
// unit-testable. The vault seals the wallet passphrase under the PIN. None of these paths deletes
|
||||
// it: a vault whose passphrase the wallet no longer accepts is only marked out of date for the
|
||||
// session (no lockout penalty), and the user removes it and sets the PIN again. The vault may hold
|
||||
// the only copy of a passphrase that still opens an older wallet backup.
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
// What a completed walletpassphrasechange means for the active wallet's PIN vault.
|
||||
enum class VaultAfterChange {
|
||||
Keep, // no vault, the change failed or kept the same passphrase, or the vault is another wallet's
|
||||
MarkStale, // the vault seals the OLD passphrase: stop offering the PIN this session and warn (keep the file)
|
||||
};
|
||||
|
||||
// Decided on the main thread when the change completes, not when it is sent, so a vault created
|
||||
// while the change was in flight is covered too. `pathAtDispatch` is the vault path captured when
|
||||
// the change was sent and `pathNow` the active vault path now: a wallet switch in between makes
|
||||
// them differ, and the other wallet's vault is left alone.
|
||||
inline VaultAfterChange decideVaultAfterPassphraseChange(bool changeSucceeded,
|
||||
bool vaultExistsNow,
|
||||
bool samePassphrase,
|
||||
const std::string& pathAtDispatch,
|
||||
const std::string& pathNow) {
|
||||
if (!changeSucceeded || !vaultExistsNow || samePassphrase) return VaultAfterChange::Keep;
|
||||
if (pathAtDispatch.empty() || pathAtDispatch != pathNow) return VaultAfterChange::Keep;
|
||||
return VaultAfterChange::MarkStale;
|
||||
}
|
||||
|
||||
// dragonxd's RPC_WALLET_PASSPHRASE_INCORRECT ("The wallet passphrase entered was incorrect.").
|
||||
constexpr int kRpcWalletPassphraseIncorrect = -14;
|
||||
|
||||
// A PIN unlock whose vault opened (so the PIN was right) but whose walletpassphrase call the daemon
|
||||
// refused with -14: the vault holds a passphrase the wallet no longer accepts (it was changed after
|
||||
// the PIN was set, e.g. with dragonx-cli walletpassphrasechange). Not a wrong-PIN attempt, so no
|
||||
// lockout penalty; the PIN is marked out of date and the vault is kept.
|
||||
inline bool isStaleVaultUnlockFailure(bool vaultDecrypted, int rpcCode) {
|
||||
return vaultDecrypted && rpcCode == kRpcWalletPassphraseIncorrect;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user