#include "wallet_security_controller.h" #include "../util/secure_vault.h" #include "../util/address_validation.h" #include #include #include #include #include namespace dragonx { namespace services { WalletSecurityController::~WalletSecurityController() { clearDeferredEncryption(); } void WalletSecurityController::beginDeferredEncryption(std::string passphrase, std::string pin) { clearDeferredEncryption(); deferred_.passphrase = std::move(passphrase); deferred_.pin = std::move(pin); deferred_.pending = true; deferred_.lastConnectAttempt = -10.0; } WalletSecurityController::DeferredEncryptionSnapshot WalletSecurityController::deferredEncryption() const { return {deferred_.passphrase, deferred_.pin}; } bool WalletSecurityController::shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds) { if (!deferred_.pending) return false; if (nowSeconds - deferred_.lastConnectAttempt < minIntervalSeconds) return false; deferred_.lastConnectAttempt = nowSeconds; return true; } void WalletSecurityController::clearDeferredEncryption() { secureClear(deferred_.passphrase); secureClear(deferred_.pin); deferred_.pending = false; deferred_.lastConnectAttempt = -10.0; } WalletSecurityController::DeferredEncryptionResult WalletSecurityController::runDeferredEncryption( DeferredEncryptionSnapshot request, RpcGateway& rpc, VaultGateway* vault) { DeferredEncryptionResult result; result.pinProvided = !request.pin.empty(); std::string error; if (!rpc.encryptWallet(request.passphrase, error)) { result.error = error.empty() ? "encryptwallet failed" : error; secureClear(request.passphrase); secureClear(request.pin); return result; } result.encrypted = true; result.restartRequired = true; if (result.pinProvided && vault) { result.pinStored = vault->storePin(request.pin, request.passphrase); } secureClear(request.passphrase); secureClear(request.pin); return result; } WalletSecurityController::PinValidationResult WalletSecurityController::validatePinSetup( const std::string& pin, const std::string& confirmation, bool allowEmpty, std::size_t minLength) { if (pin.empty() && confirmation.empty()) { return allowEmpty ? PinValidationResult{true, PinValidationError::None, ""} : PinValidationResult{false, PinValidationError::Empty, "PIN is required"}; } if (pin != confirmation) { return {false, PinValidationError::Mismatch, "PINs do not match"}; } if (pin.size() < minLength) { return {false, PinValidationError::TooShort, "PIN is too short"}; } for (unsigned char c : pin) { if (!std::isdigit(c)) { return {false, PinValidationError::NonDigit, "PIN must contain only digits"}; } } return {true, PinValidationError::None, ""}; } WalletSecurityController::KeyKind WalletSecurityController::classifyAddress(const std::string& address) { return !address.empty() && address[0] == 'z' ? KeyKind::Shielded : KeyKind::Transparent; } WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(const std::string& key) { // Shielded: Sapling extended spending key (secret-extended-key-...) or legacy Sprout (SK...). // (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.) if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded; if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded; // A "z"-prefixed key is a shielded viewing key (zxview…); "s"-prefixed is a legacy Sprout key. if (!key.empty() && (key[0] == 's' || key[0] == 'z')) return KeyKind::Shielded; return KeyKind::Transparent; } bool WalletSecurityController::isViewingKey(const std::string& key) { // DragonX's z_exportviewingkey returns a Sapling *incoming* viewing key (mainnet HRP "zivks"); // z_importviewingkey only decodes that form. Recognize it structurally — a valid Bech32 checksum // plus a known HRP — instead of a bare prefix, and cover testnet/regtest too. (The old check // looked for Zcash's "zxview" extended-FVK HRP, which DragonX never emits, so every real viewing // key was rejected client-side.) Watch-only: reveals the address's funds but cannot spend them. const std::string hrp = util::bech32Hrp(key); return hrp == "zivks" // mainnet || hrp == "zivktestsapling" // testnet || hrp == "zivkregtestsapling"; // regtest } bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key) { // Sapling z spending key (HRP "secret-extended-key-{main,test,regtest}"). These run ~300 chars, // past the Bech32 length cap, so match by HRP prefix and let the daemon vet the payload. if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return true; // Sprout z spending key // Transparent WIF: decode Base58Check and confirm it is actually a secret key — version byte plus // a 32-byte key, optionally a compression flag (payload 33 or 34 bytes). This accepts BOTH the // compressed ("U…") and uncompressed ("7…") mainnet forms and the testnet form, and rejects // addresses / typos via the real checksum — the old length+first-char heuristic dropped the // uncompressed mainnet key (which starts with '7', not one of 5/K/L/U). std::vector payload; if (util::decodeBase58Check(key, payload) && (payload.size() == 33 || payload.size() == 34) && (payload[0] == 188 /* DragonX main/regtest SECRET_KEY */ || payload[0] == 128 /* DragonX testnet SECRET_KEY */)) { return true; } return false; } bool WalletSecurityController::isRecognizedImportKey(const std::string& key) { return isRecognizedPrivateKey(key) || isViewingKey(key); } const char* WalletSecurityController::importSuccessMessage(KeyKind kind) { return kind == KeyKind::Shielded ? "Z-address key imported successfully. Wallet is rescanning." : "T-address key imported successfully. Wallet is rescanning."; } std::string WalletSecurityController::decryptExportFileName(std::uint64_t timestampSeconds) { char buffer[64]; snprintf(buffer, sizeof(buffer), "obsidiandecryptexport%llu", static_cast(timestampSeconds)); return std::string(buffer); } void WalletSecurityController::secureClear(std::string& value) { if (!value.empty()) { util::SecureVault::secureZero(&value[0], value.size()); value.clear(); } } } // namespace services } // namespace dragonx