Diagnostics & logging: - add verbose logging system (VERBOSE_LOGF) with toggle in Settings - forward app-level log messages to Console tab for in-UI visibility - add detailed connection attempt logging (attempt #, daemon state, config paths, auth failures, port owner identification) - detect HTTP 401 auth failures and show actionable error messages - identify port owner process (PID + name) on both Linux and Windows - demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF - persist verbose_logging preference in settings.json - link iphlpapi on Windows for GetExtendedTcpTable Security & encryption: - update local encryption state immediately after encryptwallet RPC so Settings reflects the change before daemon restarts - show notifications for encrypt success/failure and PIN skip - use dedicated RPC client for z_importwallet during decrypt flow to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh) - force full state refresh (addresses, transactions, peers) after successful wallet import Network tab: - redesign peers refresh button as glass-panel with icon + label, matching the mining button style - add spinning arc animation while peer data is loading (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo) - prevent double-click spam during refresh - add refresh-button size to ui.toml Other: - use fast_rpc_ for rescan polling to avoid blocking on main rpc_ - enable DRAGONX_DEBUG in all build configs (was debug-only) - setup.sh: pull latest xmrig-hac when repo already exists
1661 lines
80 KiB
C++
1661 lines
80 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// app_security.cpp — Wallet encryption, lock screen, PIN management
|
|
// Split from app.cpp for maintainability.
|
|
|
|
#include "app.h"
|
|
#include "rpc/rpc_client.h"
|
|
#include "rpc/rpc_worker.h"
|
|
#include "rpc/connection.h"
|
|
#include "config/settings.h"
|
|
#include "daemon/embedded_daemon.h"
|
|
#include "ui/notifications.h"
|
|
#include "ui/material/color_theme.h"
|
|
#include "ui/material/type.h"
|
|
#include "ui/material/typography.h"
|
|
#include "ui/material/draw_helpers.h"
|
|
#include "ui/schema/ui_schema.h"
|
|
#include "ui/theme.h"
|
|
#include "ui/effects/imgui_acrylic.h"
|
|
#include "util/platform.h"
|
|
#include "util/secure_vault.h"
|
|
#include "util/perf_log.h"
|
|
#include "embedded/IconsMaterialDesign.h"
|
|
|
|
#include "imgui.h"
|
|
#include <nlohmann/json.hpp>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
|
|
namespace dragonx {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
|
|
// ===========================================================================
|
|
// Wallet encryption helpers
|
|
// ===========================================================================
|
|
|
|
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
|
if (!rpc_ || !rpc_->isConnected()) return;
|
|
encrypt_in_progress_ = true;
|
|
encrypt_status_ = "Encrypting wallet...";
|
|
|
|
if (worker_) {
|
|
worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb {
|
|
try {
|
|
auto result = rpc_->call("encryptwallet", {passphrase});
|
|
return [this]() {
|
|
encrypt_in_progress_ = false;
|
|
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
|
|
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
|
|
|
|
// Immediately update local encryption state so the
|
|
// settings page reflects that the wallet is now encrypted
|
|
// (the daemon is about to restart, so getwalletinfo won't
|
|
// be available for a while).
|
|
state_.encrypted = true;
|
|
state_.locked = true;
|
|
state_.unlocked_until = 0;
|
|
state_.encryption_state_known = true;
|
|
|
|
// Transition settings dialog to PIN setup phase
|
|
if (show_encrypt_dialog_ &&
|
|
encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
|
encrypt_dialog_phase_ = EncryptDialogPhase::PinSetup;
|
|
}
|
|
|
|
ui::Notifications::instance().info(
|
|
"Wallet encrypted successfully");
|
|
|
|
// The daemon shuts itself down after encryptwallet
|
|
if (isUsingEmbeddedDaemon()) {
|
|
// Give daemon a moment to shut down, then restart
|
|
// (do this off the main thread to avoid stalling the UI)
|
|
std::thread([this]() {
|
|
std::this_thread::sleep_for(std::chrono::seconds(2));
|
|
stopEmbeddedDaemon();
|
|
startEmbeddedDaemon();
|
|
// tryConnect will be called by the update loop
|
|
}).detach();
|
|
} else {
|
|
ui::Notifications::instance().warning(
|
|
"Please restart your daemon for encryption to take effect.");
|
|
}
|
|
};
|
|
} catch (const std::exception& e) {
|
|
std::string err = e.what();
|
|
return [this, err]() {
|
|
encrypt_in_progress_ = false;
|
|
encrypt_status_ = "Encryption failed: " + err;
|
|
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
|
|
|
|
ui::Notifications::instance().error(
|
|
"Encryption failed: " + err);
|
|
|
|
// Return to passphrase entry on failure
|
|
if (show_encrypt_dialog_ &&
|
|
encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
|
encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Deferred encryption — runs after wizard exits, encrypts wallet in background
|
|
// Called every frame from render() until the task completes.
|
|
// ---------------------------------------------------------------------------
|
|
void App::processDeferredEncryption() {
|
|
if (!deferred_encrypt_pending_) return;
|
|
|
|
// Phase 1: wait for daemon connection
|
|
if (!state_.connected || !rpc_ || !rpc_->isConnected()) {
|
|
// Throttle connection attempts to every 3 seconds
|
|
static double s_lastAttempt = -10.0;
|
|
double now = ImGui::GetTime();
|
|
if (now - s_lastAttempt >= 3.0) {
|
|
s_lastAttempt = now;
|
|
if (!connection_in_progress_) {
|
|
// Just try to connect — tryConnect is now async
|
|
tryConnect();
|
|
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
|
startEmbeddedDaemon();
|
|
}
|
|
}
|
|
}
|
|
return; // try again next frame
|
|
}
|
|
|
|
// Phase 2: connected — launch encryption
|
|
if (!encrypt_in_progress_) {
|
|
std::string passphrase = deferred_encrypt_passphrase_;
|
|
std::string pin = deferred_encrypt_pin_;
|
|
|
|
encrypt_in_progress_ = true;
|
|
encrypt_status_ = "Encrypting wallet...";
|
|
|
|
if (worker_) {
|
|
worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb {
|
|
try {
|
|
rpc_->call("encryptwallet", {passphrase});
|
|
|
|
// Store PIN vault on the worker thread (Argon2id is expensive)
|
|
bool pinStored = false;
|
|
if (!pin.empty() && vault_) {
|
|
pinStored = vault_->store(pin, passphrase);
|
|
}
|
|
|
|
return [this, pinStored, pin]() {
|
|
encrypt_in_progress_ = false;
|
|
encrypt_status_.clear();
|
|
DEBUG_LOGF("[App] Wallet encrypted (deferred)\n");
|
|
|
|
// Finalize PIN settings on main thread
|
|
if (!pin.empty()) {
|
|
if (pinStored) {
|
|
settings_->setPinEnabled(true);
|
|
settings_->save();
|
|
ui::Notifications::instance().info("Wallet encrypted & PIN set");
|
|
} else {
|
|
ui::Notifications::instance().warning(
|
|
"Wallet encrypted but PIN vault failed");
|
|
}
|
|
} else {
|
|
ui::Notifications::instance().info("Wallet encrypted successfully");
|
|
}
|
|
|
|
// Securely clear deferred state
|
|
if (!deferred_encrypt_passphrase_.empty()) {
|
|
util::SecureVault::secureZero(
|
|
&deferred_encrypt_passphrase_[0],
|
|
deferred_encrypt_passphrase_.size());
|
|
deferred_encrypt_passphrase_.clear();
|
|
}
|
|
if (!deferred_encrypt_pin_.empty()) {
|
|
util::SecureVault::secureZero(
|
|
&deferred_encrypt_pin_[0],
|
|
deferred_encrypt_pin_.size());
|
|
deferred_encrypt_pin_.clear();
|
|
}
|
|
deferred_encrypt_pending_ = false;
|
|
|
|
// Restart daemon (it shuts itself down after encryptwallet)
|
|
if (isUsingEmbeddedDaemon()) {
|
|
std::thread([this]() {
|
|
std::this_thread::sleep_for(std::chrono::seconds(2));
|
|
stopEmbeddedDaemon();
|
|
startEmbeddedDaemon();
|
|
// tryConnect will be called by the update loop
|
|
}).detach();
|
|
} else {
|
|
ui::Notifications::instance().warning(
|
|
"Please restart your daemon for encryption to take effect.");
|
|
}
|
|
};
|
|
} catch (const std::exception& e) {
|
|
std::string err = e.what();
|
|
return [this, err]() {
|
|
encrypt_in_progress_ = false;
|
|
encrypt_status_ = "Encryption failed: " + err;
|
|
deferred_encrypt_pending_ = false;
|
|
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
|
|
ui::Notifications::instance().error("Encryption failed: " + err);
|
|
|
|
// Clean up sensitive data on failure
|
|
if (!deferred_encrypt_passphrase_.empty()) {
|
|
util::SecureVault::secureZero(
|
|
&deferred_encrypt_passphrase_[0],
|
|
deferred_encrypt_passphrase_.size());
|
|
deferred_encrypt_passphrase_.clear();
|
|
}
|
|
if (!deferred_encrypt_pin_.empty()) {
|
|
util::SecureVault::secureZero(
|
|
&deferred_encrypt_pin_[0],
|
|
deferred_encrypt_pin_.size());
|
|
deferred_encrypt_pin_.clear();
|
|
}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void App::unlockWallet(const std::string& passphrase, int timeout) {
|
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
|
lock_unlock_in_progress_ = true;
|
|
|
|
// Use fast-lane worker to bypass head-of-line blocking behind refreshData.
|
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
|
w->post([this, r, passphrase, timeout]() -> rpc::RPCWorker::MainCb {
|
|
bool ok = false;
|
|
std::string err_msg;
|
|
try {
|
|
r->call("walletpassphrase", {passphrase, timeout});
|
|
ok = true;
|
|
} catch (const std::exception& e) {
|
|
err_msg = e.what();
|
|
}
|
|
|
|
return [this, ok, err_msg, timeout]() {
|
|
lock_unlock_in_progress_ = false;
|
|
if (ok) {
|
|
lock_error_msg_.clear();
|
|
lock_attempts_ = 0;
|
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
|
last_interaction_ = std::chrono::steady_clock::now();
|
|
// Set unlock state immediately — walletpassphrase
|
|
// already succeeded, no need for another RPC round-trip.
|
|
state_.encrypted = true;
|
|
state_.locked = false;
|
|
state_.unlocked_until = std::time(nullptr) + timeout;
|
|
} else {
|
|
lock_attempts_++;
|
|
lock_error_msg_ = "Incorrect passphrase";
|
|
lock_error_timer_ = 3.0f;
|
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
|
|
|
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
|
int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
|
|
if (lock_attempts_ >= maxAttempts) {
|
|
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
|
|
}
|
|
DEBUG_LOGF("[App] Wallet unlock failed (attempt %d): %s\n", lock_attempts_, err_msg.c_str());
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::lockWallet() {
|
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
|
if (lock_unlock_in_progress_) return; // Prevent duplicate async calls
|
|
lock_unlock_in_progress_ = true;
|
|
|
|
// Use fast-lane worker to avoid blocking behind refreshData.
|
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
|
w->post([this, r]() -> rpc::RPCWorker::MainCb {
|
|
bool ok = false;
|
|
try {
|
|
r->call("walletlock");
|
|
ok = true;
|
|
} catch (...) {}
|
|
|
|
return [this, ok]() {
|
|
lock_unlock_in_progress_ = false;
|
|
if (ok) {
|
|
state_.locked = true;
|
|
state_.unlocked_until = 0;
|
|
DEBUG_LOGF("[App] Wallet locked\n");
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
|
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
|
encrypt_in_progress_ = true;
|
|
encrypt_status_ = "Changing passphrase...";
|
|
|
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
|
w->post([this, r, oldPass, newPass]() -> rpc::RPCWorker::MainCb {
|
|
bool ok = false;
|
|
std::string err_msg;
|
|
try {
|
|
r->call("walletpassphrasechange", {oldPass, newPass});
|
|
ok = true;
|
|
} catch (const std::exception& e) {
|
|
err_msg = e.what();
|
|
}
|
|
|
|
return [this, ok, err_msg]() {
|
|
encrypt_in_progress_ = false;
|
|
if (ok) {
|
|
encrypt_status_.clear();
|
|
show_change_passphrase_ = false;
|
|
memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_));
|
|
memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_));
|
|
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
|
|
ui::Notifications::instance().info("Passphrase changed successfully");
|
|
} else {
|
|
encrypt_status_ = "Failed: " + err_msg;
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Refresh wallet encryption state (from getwalletinfo)
|
|
// ===========================================================================
|
|
|
|
void App::refreshWalletEncryptionState() {
|
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
|
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
json result;
|
|
bool ok = false;
|
|
try {
|
|
result = rpc_->call("getwalletinfo");
|
|
ok = true;
|
|
} catch (...) {}
|
|
|
|
if (!ok) return nullptr;
|
|
|
|
return [this, result]() {
|
|
try {
|
|
if (result.contains("unlocked_until")) {
|
|
state_.encrypted = true;
|
|
int64_t until = result["unlocked_until"].get<int64_t>();
|
|
state_.unlocked_until = until;
|
|
state_.locked = (until == 0);
|
|
} else {
|
|
state_.encrypted = false;
|
|
state_.locked = false;
|
|
state_.unlocked_until = 0;
|
|
|
|
// Wallet is no longer encrypted — if a PIN vault exists,
|
|
// it's stale (passphrase it protects is gone). Reset PIN
|
|
// as if it were never set.
|
|
if (vault_ && vault_->hasVault()) {
|
|
DEBUG_LOGF("[App] Wallet unencrypted but PIN vault exists — removing stale vault\n");
|
|
vault_->removeVault();
|
|
}
|
|
if (settings_ && settings_->getPinEnabled()) {
|
|
settings_->setPinEnabled(false);
|
|
settings_->save();
|
|
}
|
|
}
|
|
state_.encryption_state_known = true;
|
|
} catch (...) {}
|
|
};
|
|
});
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Auto-lock on idle
|
|
// ===========================================================================
|
|
|
|
void App::checkAutoLock() {
|
|
if (!state_.isEncrypted() || state_.isLocked()) return;
|
|
|
|
// Don't auto-lock while mining — mining is a long-running intentional
|
|
// operation and locking the wallet on the daemon side stops solo mining.
|
|
bool miningActive = state_.mining.generate
|
|
|| (xmrig_manager_ && xmrig_manager_->isRunning());
|
|
if (miningActive) {
|
|
// Keep resetting the idle timer so we don't lock the instant mining stops
|
|
last_interaction_ = std::chrono::steady_clock::now();
|
|
return;
|
|
}
|
|
|
|
int timeout = settings_ ? settings_->getAutoLockTimeout() : 300;
|
|
if (timeout <= 0) return; // disabled
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
float elapsed = std::chrono::duration<float>(now - last_interaction_).count();
|
|
|
|
if (elapsed >= (float)timeout) {
|
|
lockWallet();
|
|
DEBUG_LOGF("[App] Auto-locked wallet after %d seconds idle\n", timeout);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Restart Setup Wizard (from Settings)
|
|
// ===========================================================================
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
// Lock Screen Rendering
|
|
// ===========================================================================
|
|
|
|
void App::renderLockScreen() {
|
|
using namespace ui::material;
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 winPos = ImGui::GetWindowPos();
|
|
ImVec2 winSize = ImGui::GetWindowSize();
|
|
|
|
// Optional backdrop (0 = no darkening)
|
|
float backdropAlpha = ui::schema::UI().drawElement("screens.lock-screen", "backdrop-alpha").opacity;
|
|
if (backdropAlpha > 0.0f) {
|
|
ImU32 backdropCol = IM_COL32(0, 0, 0, (int)(255 * backdropAlpha));
|
|
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), backdropCol);
|
|
}
|
|
|
|
// Card
|
|
const auto& S = ui::schema::UI();
|
|
float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f);
|
|
float cardH = S.drawElement("screens.lock-screen", "card").height;
|
|
if (cardW <= 0) cardW = 400.0f;
|
|
if (cardH <= 0) cardH = 320.0f;
|
|
|
|
float cardX = winPos.x + (winSize.x - cardW) * 0.5f;
|
|
float cardY = winPos.y + (winSize.y - cardH) * 0.5f;
|
|
ImVec2 cardMin(cardX, cardY);
|
|
ImVec2 cardMax(cardX + cardW, cardY + cardH);
|
|
|
|
ImU32 cardBg = ui::material::SurfaceVariant();
|
|
dl->AddRectFilled(cardMin, cardMax, cardBg, 16.0f);
|
|
|
|
float cy = cardY + 24.0f;
|
|
|
|
// Logo
|
|
float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f);
|
|
if (logo_tex_ != 0) {
|
|
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
|
float logoW = logoSize * aspect;
|
|
float logoX = cardX + (cardW - logoW) * 0.5f;
|
|
dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize));
|
|
}
|
|
cy += logoSize + 16.0f;
|
|
|
|
// Title
|
|
ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font);
|
|
if (!titleFont) titleFont = ui::material::Type().h5();
|
|
ImFont* captionFont = S.resolveFont(S.label("screens.lock-screen", "error-text").font);
|
|
if (!captionFont) captionFont = ui::material::Type().caption();
|
|
ImU32 textCol = ui::material::OnSurface();
|
|
|
|
{
|
|
const char* title = "Wallet Locked";
|
|
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
|
|
dl->AddText(titleFont, titleFont->LegacySize,
|
|
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
|
|
cy += ts.y + 20.0f;
|
|
}
|
|
|
|
// Lockout timer
|
|
if (lock_lockout_timer_ > 0.0f) {
|
|
lock_lockout_timer_ -= ImGui::GetIO().DeltaTime;
|
|
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
|
|
|
|
char msg[128];
|
|
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
|
|
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
|
dl->AddText(captionFont, captionFont->LegacySize,
|
|
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
|
|
cy += captionFont->LegacySize + 12.0f;
|
|
}
|
|
|
|
// Check if PIN vault is available
|
|
bool hasPinVault = vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled();
|
|
|
|
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
|
if (hasPinVault) {
|
|
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
|
|
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
|
|
const char* switchLabel = lock_use_pin_
|
|
? "Use passphrase instead"
|
|
: "Use PIN instead";
|
|
|
|
// Current mode indicator — icon with icon font, text with caption font
|
|
ImFont* iconFont = ui::material::Type().iconSmall();
|
|
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, modeIcon);
|
|
ImVec2 textSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, modeText);
|
|
float totalW = iconSize.x + textSize.x;
|
|
float startX = cardX + (cardW - totalW) * 0.5f;
|
|
float textY = cy + (iconSize.y - textSize.y) * 0.5f; // vertically align text to icon
|
|
dl->AddText(iconFont, iconFont->LegacySize,
|
|
ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon);
|
|
dl->AddText(captionFont, captionFont->LegacySize,
|
|
ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText);
|
|
cy += std::max(iconSize.y, textSize.y) + 8.0f;
|
|
|
|
// Switch link
|
|
ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel);
|
|
float switchX = cardX + (cardW - sls.x) * 0.5f;
|
|
ImGui::SetCursorScreenPos(ImVec2(switchX, cy));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
|
if (ImGui::InvisibleButton("##switch_mode", sls)) {
|
|
lock_use_pin_ = !lock_use_pin_;
|
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
|
memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_));
|
|
lock_error_msg_.clear();
|
|
lock_screen_was_visible_ = false; // re-trigger auto-focus for new input
|
|
}
|
|
dl->AddText(captionFont, captionFont->LegacySize,
|
|
ImVec2(switchX, cy), ui::material::Primary(), switchLabel);
|
|
ImGui::PopStyleColor();
|
|
cy += captionFont->LegacySize + 12.0f;
|
|
} else {
|
|
// No PIN vault — don't show toggle, force passphrase mode
|
|
lock_use_pin_ = false;
|
|
}
|
|
|
|
// Input field
|
|
float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f);
|
|
if (inputW <= 0) inputW = 320.0f;
|
|
float inputX = cardX + (cardW - inputW) * 0.5f;
|
|
|
|
bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_;
|
|
bool submitted = false;
|
|
|
|
if (lock_use_pin_ && hasPinVault) {
|
|
// PIN input
|
|
ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
|
|
ImGui::PushItemWidth(inputW);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
|
ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal;
|
|
if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
|
submitted = ImGui::InputText("##lock_pin", lock_pin_buf_,
|
|
sizeof(lock_pin_buf_), pinFlags);
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopItemWidth();
|
|
} else {
|
|
// Passphrase input (original)
|
|
ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
|
|
ImGui::PushItemWidth(inputW);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
|
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password;
|
|
if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
|
submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_,
|
|
sizeof(lock_passphrase_buf_), inputFlags);
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopItemWidth();
|
|
}
|
|
cy += 40.0f + 12.0f;
|
|
|
|
// Focus the input when the lock screen first appears.
|
|
// IsWindowAppearing() does not work here because the lock screen is
|
|
// rendered inside ##ContentArea which has already been alive since the
|
|
// first frame. Instead we track the hidden→visible transition ourselves.
|
|
if (!lock_screen_was_visible_) {
|
|
ImGui::SetKeyboardFocusHere(-1);
|
|
lock_screen_was_visible_ = true;
|
|
}
|
|
|
|
// Error message
|
|
if (!lock_error_msg_.empty() && lock_error_timer_ > 0) {
|
|
lock_error_timer_ -= ImGui::GetIO().DeltaTime;
|
|
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str());
|
|
dl->AddText(captionFont, captionFont->LegacySize,
|
|
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
|
lock_error_msg_.c_str());
|
|
cy += captionFont->LegacySize + 8.0f;
|
|
}
|
|
|
|
// "Unlocking..." feedback while worker thread is running
|
|
// Always reserve the vertical space so the button doesn't shift.
|
|
{
|
|
float rowH = captionFont->LegacySize + 8.0f;
|
|
if (lock_unlock_in_progress_) {
|
|
// Animated spinner dots
|
|
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
const char* dotStr[] = {"", ".", "..", "..."};
|
|
char msg[64];
|
|
snprintf(msg, sizeof(msg), "Unlocking%s", dotStr[dots]);
|
|
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
|
dl->AddText(captionFont, captionFont->LegacySize,
|
|
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
|
|
ui::material::Primary(), msg);
|
|
}
|
|
cy += rowH;
|
|
}
|
|
|
|
// Unlock button
|
|
float unlockW = S.drawElement("screens.lock-screen", "unlock-button").getFloat("width", 320.0f);
|
|
float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height;
|
|
if (unlockW <= 0) unlockW = 320.0f;
|
|
if (unlockH <= 0) unlockH = 44.0f;
|
|
float unlockX = cardX + (cardW - unlockW) * 0.5f;
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(unlockX, cy));
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
|
ImGui::BeginDisabled(!canSubmit);
|
|
bool btnClicked = ImGui::Button("Unlock", ImVec2(unlockW, unlockH));
|
|
ImGui::EndDisabled();
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor(3);
|
|
|
|
// Handle submit (Enter key or button click)
|
|
if ((submitted || btnClicked) && canSubmit) {
|
|
// Derive daemon unlock timeout from auto-lock setting:
|
|
// 2x the idle timeout so the daemon stays unlocked longer than
|
|
// the GUI auto-lock, acting as a safety net if the GUI crashes.
|
|
// Floor of 600s (10 min) when auto-lock is off or very short.
|
|
int autoLock = settings_ ? settings_->getAutoLockTimeout() : 300;
|
|
int timeout = (autoLock > 0) ? std::max(600, autoLock * 2) : 86400;
|
|
|
|
if (lock_use_pin_ && hasPinVault && strlen(lock_pin_buf_) > 0) {
|
|
// PIN unlock — run Argon2id key derivation + RPC off the main
|
|
// thread so the UI stays responsive instead of freezing.
|
|
std::string pin(lock_pin_buf_);
|
|
memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_));
|
|
lock_unlock_in_progress_ = true;
|
|
|
|
// Use fast-lane worker for priority unlock.
|
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
|
if (w) {
|
|
w->post([this, r, pin, timeout]() -> rpc::RPCWorker::MainCb {
|
|
// Heavy Argon2id derivation runs here (worker thread)
|
|
std::string passphrase;
|
|
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
|
|
|
|
if (!vaultOk) {
|
|
return [this]() {
|
|
lock_unlock_in_progress_ = false;
|
|
lock_attempts_++;
|
|
lock_error_msg_ = "Incorrect PIN";
|
|
lock_error_timer_ = 3.0f;
|
|
|
|
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
|
int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
|
|
if (lock_attempts_ >= maxAttempts) {
|
|
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
|
|
}
|
|
};
|
|
}
|
|
|
|
// Vault decrypted — now unlock wallet via RPC (also on worker thread)
|
|
bool rpcOk = false;
|
|
std::string rpcErr;
|
|
try {
|
|
if (r && r->isConnected()) {
|
|
r->call("walletpassphrase", {passphrase, timeout});
|
|
rpcOk = true;
|
|
} else {
|
|
rpcErr = "Not connected to daemon";
|
|
}
|
|
} catch (const std::exception& e) {
|
|
rpcErr = e.what();
|
|
}
|
|
|
|
// Securely wipe passphrase
|
|
util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
|
|
|
if (rpcOk) {
|
|
return [this, timeout]() {
|
|
lock_unlock_in_progress_ = false;
|
|
lock_error_msg_.clear();
|
|
lock_attempts_ = 0;
|
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
|
last_interaction_ = std::chrono::steady_clock::now();
|
|
// Set unlock state immediately — walletpassphrase
|
|
// already succeeded, no need for another RPC round-trip.
|
|
state_.encrypted = true;
|
|
state_.locked = false;
|
|
state_.unlocked_until = std::time(nullptr) + timeout;
|
|
};
|
|
} else {
|
|
return [this, rpcErr]() {
|
|
lock_unlock_in_progress_ = false;
|
|
lock_attempts_++;
|
|
lock_error_msg_ = "Unlock failed: " + rpcErr;
|
|
lock_error_timer_ = 3.0f;
|
|
};
|
|
}
|
|
});
|
|
}
|
|
} else if (strlen(lock_passphrase_buf_) > 0) {
|
|
// Direct passphrase unlock
|
|
unlockWallet(std::string(lock_passphrase_buf_), timeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Encrypt Wallet Dialog (post-first-run, from Settings)
|
|
// ===========================================================================
|
|
|
|
void App::renderEncryptWalletDialog() {
|
|
if (!show_encrypt_dialog_ && !show_change_passphrase_) return;
|
|
using namespace ui::material;
|
|
|
|
// Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
|
|
if (show_encrypt_dialog_) {
|
|
const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
|
|
? "Quick-Unlock PIN" : "Encrypt Wallet";
|
|
|
|
// Prevent closing via X button while encrypting
|
|
bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
|
|
bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;
|
|
|
|
if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) {
|
|
|
|
// ---- Phase 1: Passphrase entry ----
|
|
if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
|
ImGui::TextWrapped(ICON_MD_WARNING
|
|
" If you lose your passphrase, you lose access to your funds.");
|
|
ImGui::PopStyleColor();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextWrapped("Encrypting your wallet protects your private keys "
|
|
"with a passphrase. After encryption, the daemon will restart.");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Passphrase:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("Confirm:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::PopItemWidth();
|
|
|
|
// Strength meter bar
|
|
{
|
|
size_t len = strlen(encrypt_pass_buf_);
|
|
const char* strengthLabel = "Weak";
|
|
ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
|
|
float strengthPct = 0.25f;
|
|
if (len >= 16) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
|
else if (len >= 12) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
|
else if (len >= 8) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
|
|
|
float barW = ImGui::GetContentRegionAvail().x;
|
|
float barH = 4.0f;
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
|
IM_COL32(255,255,255,30), 2.0f);
|
|
if (len > 0)
|
|
dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH),
|
|
ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f);
|
|
ImGui::Dummy(ImVec2(barW, barH));
|
|
ImGui::Text("Strength: %s", strengthLabel);
|
|
}
|
|
|
|
if (!encrypt_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
bool valid = strlen(encrypt_pass_buf_) >= 8 &&
|
|
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
|
|
|
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
|
if (ImGui::Button("Encrypt Wallet", ImVec2(btnW, 40))) {
|
|
std::string pass(encrypt_pass_buf_);
|
|
enc_dlg_saved_passphrase_ = pass;
|
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
|
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
|
encrypt_dialog_phase_ = EncryptDialogPhase::Encrypting;
|
|
encryptWalletWithPassphrase(pass);
|
|
util::SecureVault::secureZero(&pass[0], pass.size());
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
|
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
|
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
|
show_encrypt_dialog_ = false;
|
|
}
|
|
|
|
// ---- Phase 2: Encrypting in progress ----
|
|
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
|
const char* statusTitle = encrypt_in_progress_
|
|
? "Encrypting wallet..." : encrypt_status_.c_str();
|
|
ImGui::Text("%s", statusTitle);
|
|
ImGui::Spacing();
|
|
|
|
// Indeterminate progress bar
|
|
{
|
|
float barW = ImGui::GetContentRegionAvail().x;
|
|
float barH = 6.0f;
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
|
IM_COL32(255,255,255,25), 3.0f);
|
|
float t = (float)ImGui::GetTime();
|
|
float pulse = 0.5f + 0.5f * sinf(t * 2.0f);
|
|
float segW = barW * 0.3f;
|
|
float segX = p.x + (barW - segW) * pulse;
|
|
dl->AddRectFilled(ImVec2(segX, p.y), ImVec2(segX + segW, p.y + barH),
|
|
ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]),
|
|
3.0f);
|
|
ImGui::Dummy(ImVec2(barW, barH));
|
|
}
|
|
|
|
if (!encrypt_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 1, 1, 0.6f), "%s", encrypt_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(1,1,1,0.4f), "Please wait, do not close the application.");
|
|
|
|
// Transition to PIN phase when encryption finishes successfully
|
|
if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
|
// encryptWalletWithPassphrase callback handles the transition
|
|
}
|
|
|
|
// ---- Phase 3: PIN setup (after successful encryption) ----
|
|
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1));
|
|
ImGui::Text(ICON_MD_CHECK_CIRCLE " Wallet encrypted successfully!");
|
|
ImGui::PopStyleColor();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextWrapped("A 4-8 digit PIN lets you unlock your wallet "
|
|
"without typing the full passphrase every time.");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("PIN (4-8 digits):");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("Confirm PIN:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_,
|
|
sizeof(enc_dlg_pin_confirm_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
if (!enc_dlg_pin_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", enc_dlg_pin_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
std::string pinStr(enc_dlg_pin_buf_);
|
|
std::string pinConfirm(enc_dlg_pin_confirm_buf_);
|
|
bool pinValid = util::SecureVault::isValidPin(pinStr) && pinStr == pinConfirm;
|
|
bool hasPassphrase = !enc_dlg_saved_passphrase_.empty();
|
|
|
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
|
|
if (ImGui::Button("Set PIN", ImVec2(btnW, 40))) {
|
|
pin_in_progress_ = true;
|
|
enc_dlg_pin_status_.clear();
|
|
std::string savedPass = enc_dlg_saved_passphrase_;
|
|
if (worker_ && vault_) {
|
|
worker_->post([this, pinStr, savedPass]() -> rpc::RPCWorker::MainCb {
|
|
// Argon2id runs here (worker thread)
|
|
bool ok = vault_->store(pinStr, savedPass);
|
|
return [this, ok]() {
|
|
if (ok) {
|
|
settings_->setPinEnabled(true);
|
|
settings_->save();
|
|
pin_in_progress_ = false;
|
|
ui::Notifications::instance().info("PIN set successfully");
|
|
// Clean up
|
|
if (!enc_dlg_saved_passphrase_.empty()) {
|
|
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
|
enc_dlg_saved_passphrase_.size());
|
|
enc_dlg_saved_passphrase_.clear();
|
|
}
|
|
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
|
|
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
|
show_encrypt_dialog_ = false;
|
|
} else {
|
|
enc_dlg_pin_status_ = "Failed to create PIN vault";
|
|
pin_in_progress_ = false;
|
|
}
|
|
};
|
|
});
|
|
} else {
|
|
enc_dlg_pin_status_ = "Failed to create PIN vault";
|
|
pin_in_progress_ = false;
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Skip", ImVec2(btnW, 40))) {
|
|
if (!enc_dlg_saved_passphrase_.empty()) {
|
|
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
|
enc_dlg_saved_passphrase_.size());
|
|
enc_dlg_saved_passphrase_.clear();
|
|
}
|
|
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
|
|
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
|
show_encrypt_dialog_ = false;
|
|
ui::Notifications::instance().info(
|
|
"PIN skipped. You can set one later in Settings.");
|
|
}
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
|
|
// Clean up saved passphrase if dialog was closed via X button
|
|
if (!show_encrypt_dialog_ && !enc_dlg_saved_passphrase_.empty()) {
|
|
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
|
enc_dlg_saved_passphrase_.size());
|
|
enc_dlg_saved_passphrase_.clear();
|
|
}
|
|
}
|
|
|
|
// Change passphrase dialog
|
|
if (show_change_passphrase_) {
|
|
if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) {
|
|
|
|
ImGui::Text("Current Passphrase:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("New Passphrase:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("Confirm New:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::PopItemWidth();
|
|
|
|
if (!encrypt_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
bool valid = strlen(change_old_pass_buf_) > 0 &&
|
|
strlen(change_new_pass_buf_) >= 8 &&
|
|
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
|
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
|
if (ImGui::Button("Change Passphrase", ImVec2(-1, 40))) {
|
|
changePassphrase(std::string(change_old_pass_buf_),
|
|
std::string(change_new_pass_buf_));
|
|
}
|
|
ImGui::EndDisabled();
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Decrypt (Remove Encryption) Dialog
|
|
// ===========================================================================
|
|
// Flow:
|
|
// Phase 0: Enter current passphrase
|
|
// Phase 1: Working — unlock → export → stop → rename → restart → import
|
|
// Phase 2: Success
|
|
// Phase 3: Error
|
|
// ===========================================================================
|
|
|
|
void App::renderDecryptWalletDialog() {
|
|
if (!show_decrypt_dialog_) return;
|
|
using namespace ui::material;
|
|
|
|
bool canClose = (decrypt_phase_ != 1); // don't close while working
|
|
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
|
|
|
|
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
|
|
|
|
// ---- Phase 0: Passphrase entry ----
|
|
if (decrypt_phase_ == 0) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
|
ImGui::TextWrapped(ICON_MD_WARNING
|
|
" This will remove encryption from your wallet. "
|
|
"Your private keys will be stored unprotected on disk.");
|
|
ImGui::PopStyleColor();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextWrapped(
|
|
"The wallet will be exported, the daemon restarted with a fresh "
|
|
"unencrypted wallet, and all keys re-imported. This may take "
|
|
"several minutes depending on wallet size.");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Current Passphrase:");
|
|
ImGui::PushItemWidth(-1);
|
|
bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
|
|
sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
ImGui::PopItemWidth();
|
|
|
|
if (!decrypt_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decrypt_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
bool valid = strlen(decrypt_pass_buf_) >= 1;
|
|
|
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
ImGui::BeginDisabled(!valid || decrypt_in_progress_);
|
|
if (ImGui::Button("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
|
std::string passphrase(decrypt_pass_buf_);
|
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
|
decrypt_phase_ = 1;
|
|
decrypt_step_ = 0;
|
|
decrypt_in_progress_ = true;
|
|
decrypt_status_ = "Unlocking wallet...";
|
|
decrypt_overall_start_time_ = std::chrono::steady_clock::now();
|
|
decrypt_step_start_time_ = decrypt_overall_start_time_;
|
|
|
|
// Run entire decrypt flow on worker thread
|
|
if (worker_) {
|
|
worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb {
|
|
// Step 1: Unlock wallet
|
|
try {
|
|
rpc_->call("walletpassphrase", {passphrase, 600});
|
|
} catch (const std::exception& e) {
|
|
std::string err = e.what();
|
|
return [this, err]() {
|
|
decrypt_in_progress_ = false;
|
|
decrypt_status_ = "Incorrect passphrase";
|
|
decrypt_phase_ = 0; // back to entry
|
|
};
|
|
}
|
|
|
|
// Update step on main thread
|
|
return [this]() {
|
|
decrypt_step_ = 1;
|
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
|
decrypt_status_ = "Exporting wallet keys...";
|
|
|
|
// Continue with step 2
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
|
std::string exportFile = "obsidiandecryptexport" +
|
|
std::to_string(std::time(nullptr));
|
|
std::string exportPath = dataDir + exportFile;
|
|
|
|
try {
|
|
rpc_->call("z_exportwallet", {exportFile}, 300L);
|
|
} catch (const std::exception& e) {
|
|
std::string err = e.what();
|
|
return [this, err]() {
|
|
decrypt_in_progress_ = false;
|
|
decrypt_status_ = "Export failed: " + err;
|
|
decrypt_phase_ = 3;
|
|
};
|
|
}
|
|
|
|
return [this, exportPath]() {
|
|
decrypt_step_ = 2;
|
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
|
decrypt_status_ = "Stopping daemon...";
|
|
|
|
// Continue with step 3
|
|
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
|
|
try {
|
|
rpc_->call("stop");
|
|
} catch (...) {
|
|
// stop often throws because connection drops
|
|
}
|
|
|
|
// Wait for daemon to fully stop
|
|
std::this_thread::sleep_for(std::chrono::seconds(3));
|
|
|
|
return [this, exportPath]() {
|
|
decrypt_step_ = 3;
|
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
|
decrypt_status_ = "Backing up encrypted wallet...";
|
|
|
|
// Continue with step 4 (rename)
|
|
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
|
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
|
std::string walletPath = dataDir + "wallet.dat";
|
|
std::string backupPath = dataDir + "wallet.dat.encrypted.bak";
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(walletPath, ec)) {
|
|
std::filesystem::remove(backupPath, ec);
|
|
std::filesystem::rename(walletPath, backupPath, ec);
|
|
if (ec) {
|
|
std::string err = ec.message();
|
|
return [this, err]() {
|
|
decrypt_in_progress_ = false;
|
|
decrypt_status_ = "Failed to rename wallet.dat: " + err;
|
|
decrypt_phase_ = 3;
|
|
};
|
|
}
|
|
}
|
|
|
|
return [this, exportPath]() {
|
|
decrypt_step_ = 4;
|
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
|
decrypt_status_ = "Restarting daemon...";
|
|
|
|
auto restartAndImport = [this, exportPath]() {
|
|
std::this_thread::sleep_for(std::chrono::seconds(2));
|
|
|
|
if (isUsingEmbeddedDaemon()) {
|
|
stopEmbeddedDaemon();
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
startEmbeddedDaemon();
|
|
}
|
|
|
|
// Wait for daemon to become available
|
|
int maxWait = 60;
|
|
bool daemonUp = false;
|
|
for (int i = 0; i < maxWait; i++) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
try {
|
|
rpc_->call("getinfo");
|
|
daemonUp = true;
|
|
break;
|
|
} catch (...) {}
|
|
}
|
|
|
|
if (!daemonUp) {
|
|
if (worker_) {
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
return [this]() {
|
|
decrypt_in_progress_ = false;
|
|
decrypt_status_ = "Daemon failed to restart";
|
|
decrypt_phase_ = 3;
|
|
};
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update step on main thread — close dialog, import in background
|
|
if (worker_) {
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
return [this]() {
|
|
// Close the decrypt dialog — user can use the wallet now
|
|
decrypt_in_progress_ = false;
|
|
show_decrypt_dialog_ = false;
|
|
decrypt_import_active_ = true;
|
|
|
|
// Mark rescanning so status bar picks it up immediately
|
|
state_.sync.rescanning = true;
|
|
state_.sync.rescan_progress = 0.0f;
|
|
|
|
// Clear encryption state early — vault/PIN removed now,
|
|
// wallet file is already unencrypted
|
|
if (vault_ && vault_->hasVault()) {
|
|
vault_->removeVault();
|
|
}
|
|
if (settings_ && settings_->getPinEnabled()) {
|
|
settings_->setPinEnabled(false);
|
|
settings_->save();
|
|
}
|
|
|
|
ui::Notifications::instance().info(
|
|
"Importing keys & rescanning blockchain — wallet is usable while this runs",
|
|
8.0f);
|
|
};
|
|
});
|
|
}
|
|
|
|
// Step 6: Import wallet in background (use full path)
|
|
// Use a SEPARATE RPC client so the main rpc_'s
|
|
// curl_mutex isn't held for the entire import duration.
|
|
// Blocking rpc_ prevents refreshData/refreshPeerInfo
|
|
// from running, which leaves the UI with no peers.
|
|
auto importRpc = std::make_unique<rpc::RPCClient>();
|
|
bool importRpcOk = importRpc->connect(
|
|
saved_config_.host, saved_config_.port,
|
|
saved_config_.rpcuser, saved_config_.rpcpassword);
|
|
if (!importRpcOk) {
|
|
// Fall back to main client if temp connect fails
|
|
importRpc.reset();
|
|
}
|
|
auto* rpcForImport = importRpc ? importRpc.get() : rpc_.get();
|
|
|
|
// Use 20-minute timeout — import + rescan can be very slow
|
|
try {
|
|
rpcForImport->call("z_importwallet", {exportPath}, 1200L);
|
|
} catch (const std::exception& e) {
|
|
std::string err = e.what();
|
|
if (worker_) {
|
|
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
|
|
return [this, err]() {
|
|
decrypt_import_active_ = false;
|
|
ui::Notifications::instance().error(
|
|
"Key import failed: " + err +
|
|
"\nEncrypted backup: wallet.dat.encrypted.bak",
|
|
12.0f);
|
|
};
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Disconnect the temporary RPC client
|
|
if (importRpc) {
|
|
importRpc->disconnect();
|
|
importRpc.reset();
|
|
}
|
|
|
|
// Success — force full state refresh so peers,
|
|
// balances, and addresses are fetched immediately.
|
|
if (worker_) {
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
return [this]() {
|
|
decrypt_import_active_ = false;
|
|
|
|
// Force address + peer refresh
|
|
addresses_dirty_ = true;
|
|
transactions_dirty_ = true;
|
|
last_tx_block_height_ = -1;
|
|
|
|
refreshWalletEncryptionState();
|
|
refreshData();
|
|
refreshPeerInfo();
|
|
|
|
ui::Notifications::instance().success(
|
|
"Wallet decrypted successfully! All keys imported.",
|
|
8.0f);
|
|
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
|
};
|
|
});
|
|
}
|
|
};
|
|
|
|
std::thread(restartAndImport).detach();
|
|
};
|
|
});
|
|
};
|
|
});
|
|
};
|
|
});
|
|
};
|
|
});
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
|
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
|
show_decrypt_dialog_ = false;
|
|
}
|
|
|
|
// ---- Phase 1: Working ----
|
|
} else if (decrypt_phase_ == 1) {
|
|
// Step checklist
|
|
const char* stepLabels[] = {
|
|
"Unlocking wallet",
|
|
"Exporting wallet keys",
|
|
"Stopping daemon",
|
|
"Backing up encrypted wallet",
|
|
"Restarting daemon"
|
|
};
|
|
const int numSteps = 5;
|
|
|
|
// Compute elapsed times
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto stepElapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
now - decrypt_step_start_time_).count();
|
|
auto totalElapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
now - decrypt_overall_start_time_).count();
|
|
|
|
ImGui::Spacing();
|
|
for (int i = 0; i < numSteps; i++) {
|
|
ImGui::PushFont(Type().iconMed());
|
|
if (i < decrypt_step_) {
|
|
// Completed
|
|
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
|
} else if (i == decrypt_step_) {
|
|
// In progress - animate
|
|
float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f);
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, alpha), ICON_MD_PENDING);
|
|
} else {
|
|
// Not started
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.5f), ICON_MD_RADIO_BUTTON_UNCHECKED);
|
|
}
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
|
|
if (i == decrypt_step_) {
|
|
// Show step label with elapsed time
|
|
int mins = (int)(stepElapsed / 60);
|
|
int secs = (int)(stepElapsed % 60);
|
|
if (mins > 0) {
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
|
|
"%s... (%dm %02ds)", stepLabels[i], mins, secs);
|
|
} else {
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
|
|
"%s... (%ds)", stepLabels[i], secs);
|
|
}
|
|
} else if (i < decrypt_step_) {
|
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
|
|
} else {
|
|
ImGui::TextDisabled("%s", stepLabels[i]);
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Indeterminate progress bar
|
|
{
|
|
float barW = ImGui::GetContentRegionAvail().x;
|
|
float barH = 6.0f;
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
|
|
IM_COL32(255, 255, 255, 25), 3.0f);
|
|
float t = (float)ImGui::GetTime();
|
|
float segW = barW * 0.3f;
|
|
float x0 = p.x + (barW + segW) * (0.5f + 0.5f * sinf(t * 2.0f)) - segW;
|
|
float x1 = x0 + segW;
|
|
x0 = std::max(x0, p.x);
|
|
x1 = std::min(x1, p.x + barW);
|
|
if (x1 > x0) {
|
|
dl->AddRectFilled(ImVec2(x0, p.y), ImVec2(x1, p.y + barH),
|
|
IM_COL32(255, 218, 0, 200), 3.0f);
|
|
}
|
|
ImGui::Dummy(ImVec2(barW, barH));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Step-specific hints
|
|
if (decrypt_step_ == 4) {
|
|
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
|
|
} else {
|
|
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
|
"and re-importing. This may take several minutes.");
|
|
}
|
|
|
|
// Total elapsed
|
|
{
|
|
int tMins = (int)(totalElapsed / 60);
|
|
int tSecs = (int)(totalElapsed % 60);
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
|
|
}
|
|
|
|
// ---- Phase 2: Success ----
|
|
} else if (decrypt_phase_ == 2) {
|
|
ImGui::PushFont(Type().iconLarge());
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!");
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped(
|
|
"Your wallet is now unencrypted. A backup of the encrypted wallet "
|
|
"was saved as wallet.dat.encrypted.bak in your data directory.");
|
|
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Close", ImVec2(-1, 40))) {
|
|
show_decrypt_dialog_ = false;
|
|
}
|
|
|
|
// ---- Phase 3: Error ----
|
|
} else if (decrypt_phase_ == 3) {
|
|
ImGui::PushFont(Type().iconLarge());
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed");
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", decrypt_status_.c_str());
|
|
|
|
ImGui::Spacing();
|
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
if (ImGui::Button("Try Again", ImVec2(btnW, 40))) {
|
|
decrypt_phase_ = 0;
|
|
decrypt_step_ = 0;
|
|
decrypt_status_.clear();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Close", ImVec2(btnW, 40))) {
|
|
show_decrypt_dialog_ = false;
|
|
}
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// PIN Setup / Change / Remove Dialogs (from Settings page)
|
|
// ===========================================================================
|
|
|
|
void App::renderPinDialogs() {
|
|
using namespace ui::material;
|
|
|
|
// ---- Set PIN dialog ----
|
|
if (show_pin_setup_) {
|
|
if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) {
|
|
|
|
ImGui::TextWrapped(
|
|
"Set a 4-8 digit PIN for quick wallet unlock. "
|
|
"Your wallet passphrase will be encrypted with this PIN "
|
|
"and stored locally.");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Wallet Passphrase:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("New PIN (4-8 digits):");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("Confirm PIN:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
if (!pin_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
std::string pinStr(pin_buf_);
|
|
bool valid = strlen(pin_passphrase_buf_) > 0 &&
|
|
util::SecureVault::isValidPin(pinStr) &&
|
|
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
|
|
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
|
if (ImGui::Button("Set PIN", ImVec2(-1, 40))) {
|
|
pin_in_progress_ = true;
|
|
pin_status_ = "Verifying passphrase...";
|
|
|
|
// Verify passphrase + store vault on worker thread to avoid
|
|
// blocking the UI with Argon2id key derivation.
|
|
std::string passphrase(pin_passphrase_buf_);
|
|
std::string pin(pin_buf_);
|
|
memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_));
|
|
memset(pin_buf_, 0, sizeof(pin_buf_));
|
|
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
|
|
|
if (rpc_ && rpc_->isConnected() && worker_) {
|
|
worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb {
|
|
// Verify passphrase via RPC (worker thread)
|
|
try {
|
|
rpc_->call("walletpassphrase", {passphrase, 5});
|
|
} catch (const std::exception& e) {
|
|
return [this]() {
|
|
pin_status_ = "Incorrect passphrase";
|
|
pin_in_progress_ = false;
|
|
};
|
|
}
|
|
|
|
// Passphrase correct — store in vault (Argon2id, worker thread)
|
|
bool storeOk = vault_ && vault_->store(pin, passphrase);
|
|
|
|
// Lock wallet back
|
|
try {
|
|
rpc_->call("walletlock");
|
|
} catch (...) {}
|
|
|
|
return [this, storeOk]() {
|
|
if (storeOk) {
|
|
settings_->setPinEnabled(true);
|
|
settings_->save();
|
|
pin_status_.clear();
|
|
pin_in_progress_ = false;
|
|
show_pin_setup_ = false;
|
|
ui::Notifications::instance().info("PIN set successfully");
|
|
} else {
|
|
pin_status_ = "Failed to create vault";
|
|
pin_in_progress_ = false;
|
|
}
|
|
};
|
|
});
|
|
} else {
|
|
pin_status_ = "Not connected to daemon";
|
|
pin_in_progress_ = false;
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// ---- Change PIN dialog ----
|
|
if (show_pin_change_) {
|
|
if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) {
|
|
|
|
ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN.");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Current PIN:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("New PIN (4-8 digits):");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Text("Confirm New PIN:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
if (!pin_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
std::string newPin(pin_buf_);
|
|
bool valid = strlen(pin_old_buf_) >= 4 &&
|
|
util::SecureVault::isValidPin(newPin) &&
|
|
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
|
|
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
|
if (ImGui::Button("Change PIN", ImVec2(-1, 40))) {
|
|
pin_in_progress_ = true;
|
|
pin_status_ = "Changing PIN...";
|
|
std::string oldPin(pin_old_buf_);
|
|
std::string newPinCopy = newPin;
|
|
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
|
memset(pin_buf_, 0, sizeof(pin_buf_));
|
|
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
|
|
|
if (worker_ && vault_) {
|
|
worker_->post([this, oldPin, newPinCopy]() -> rpc::RPCWorker::MainCb {
|
|
// Argon2id runs here (worker thread)
|
|
bool ok = vault_->changePin(oldPin, newPinCopy);
|
|
return [this, ok]() {
|
|
if (ok) {
|
|
pin_status_.clear();
|
|
pin_in_progress_ = false;
|
|
show_pin_change_ = false;
|
|
ui::Notifications::instance().info("PIN changed successfully");
|
|
} else {
|
|
pin_status_ = "Incorrect current PIN";
|
|
pin_in_progress_ = false;
|
|
}
|
|
};
|
|
});
|
|
} else {
|
|
pin_status_ = "Internal error";
|
|
pin_in_progress_ = false;
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// ---- Remove PIN dialog ----
|
|
if (show_pin_remove_) {
|
|
if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) {
|
|
|
|
ImGui::TextWrapped(
|
|
"Enter your current PIN to confirm removal. "
|
|
"You will need to use your full passphrase to unlock.");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Current PIN:");
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_),
|
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
|
ImGui::PopItemWidth();
|
|
|
|
if (!pin_status_.empty()) {
|
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
bool valid = strlen(pin_old_buf_) >= 4;
|
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
|
if (ImGui::Button("Remove PIN", ImVec2(-1, 40))) {
|
|
pin_in_progress_ = true;
|
|
pin_status_ = "Verifying PIN...";
|
|
std::string oldPin(pin_old_buf_);
|
|
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
|
|
|
if (worker_ && vault_) {
|
|
worker_->post([this, oldPin]() -> rpc::RPCWorker::MainCb {
|
|
// Argon2id runs here (worker thread)
|
|
std::string passphrase;
|
|
bool ok = vault_->retrieve(oldPin, passphrase);
|
|
if (ok) {
|
|
util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
|
}
|
|
return [this, ok]() {
|
|
if (ok) {
|
|
vault_->removeVault();
|
|
settings_->setPinEnabled(false);
|
|
settings_->save();
|
|
pin_status_.clear();
|
|
pin_in_progress_ = false;
|
|
show_pin_remove_ = false;
|
|
ui::Notifications::instance().info("PIN removed");
|
|
} else {
|
|
pin_status_ = "Incorrect PIN";
|
|
pin_in_progress_ = false;
|
|
}
|
|
};
|
|
});
|
|
} else {
|
|
pin_status_ = "Internal error";
|
|
pin_in_progress_ = false;
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace dragonx
|