Rework the full-node Import Key dialog into a safer, Material-consistent, more capable flow: - Security: the key is masked by default (Password) with a reveal (eye) toggle, a DialogWarningHeader "only import a key you own" note, and the buffer is wiped with sodium_memzero on close and on successful import. - Progress: import triggers a blocking full-chain rescan; the dialog now shows "Importing & rescanning — this can take several minutes" with LoadingDots and disables Import while it runs (no double-submit). Offline shows a "connect a running node" note and disables Import up front. - Viewing keys: the classifier gains isViewingKey / isRecognizedImportKey; importPrivateKey auto-detects a shielded viewing key (zxviews…) and routes to z_importviewingkey (watch-only) vs z_importkey / importprivkey. The live type indicator shows Transparent / Shielded spending / Shielded viewing (watch-only). - Result: on success the imported address is captured from the RPC result and shown via AddressCopyField. - Material restyle (OverlayDialogSpec/BlurFloat + TactileButton + Esc) and full i18n (12 new keys x 8 languages; CJK subset rebuilt). Verified: rendered on dark + light skins; a seeded zxviews… key shows the masked field + "Shielded viewing key (watch-only)" indicator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
5.4 KiB
C++
156 lines
5.4 KiB
C++
#include "wallet_security_controller.h"
|
|
#include "../util/secure_vault.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <utility>
|
|
|
|
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)
|
|
{
|
|
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
|
|
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
|
|
return key.rfind("zxview", 0) == 0;
|
|
}
|
|
|
|
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
|
{
|
|
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: base58, ~51-52 chars, common version prefixes.
|
|
if (key.size() >= 51 && key.size() <= 52 &&
|
|
(key[0] == '5' || key[0] == 'K' || key[0] == 'L' || key[0] == 'U')) 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<unsigned long long>(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
|