From bcee4bfe72146131d347cffdfd9c46479b663fce Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 22 Jul 2026 12:02:50 -0500 Subject: [PATCH 1/2] fix(security): apply audit remediations to dev (ported + dev-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the master issues (absent on dev) plus new ones in dev-only code. Applied here; full-node build + test suite green; the subtle fixes were adversarially re-verified. Ported from the master remediation (adapted to dev's code): - bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath) before writing (S2-1). (dev already fail-closes on a missing checksum.) - xmrig updater: fail closed when a signature is required but no key is pinned (F1-1). - http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared metadata/price fetch (F1-2 / caps the updater + exchange paths at one site). - rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response cap in WriteCallback (F4-3). - lite_connection_service: reject remote http:// lite servers, loopback only (L1-1). Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with userinfo/fragment stripping. - lite controller: propagate encrypt/decrypt save() failure instead of reporting success (F7-1). - xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2). - app: clear the copied secret from the OS clipboard on shutdown (F3b-1). - export_transactions: neutralize CSV/spreadsheet formula injection (F13-1). - build pipeline: build-from-source lite backend + remove the self-attested CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4); verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success. (F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.) Dev-only findings: - rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed heap (N1-1). - seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded failed result never carries a live seed (W1-2). - export_all_keys: write the plaintext key dump 0600 + atomically via writeFileAtomically (U1-2). - chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1). Not done (need a decision, documented in the report): - Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a wire-protocol change requiring SilentDragonXLite interop review. - Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra. - Console scrollback retains console-typed key-export output in plaintext (N1-1 residual) — inherent to an echoing console; would need output redaction. Co-Authored-By: Claude Fable 5 --- CMakeLists.txt | 34 ++----- build.sh | 34 +++++-- scripts/build-lite-backend-artifact.sh | 91 +++---------------- scripts/create-appimage.sh | 27 ++++-- setup.sh | 23 ++++- src/app.cpp | 17 +++- src/app.h | 3 + src/chat/chat_database.cpp | 11 +++ src/daemon/seed_wallet_creator.cpp | 8 ++ src/daemon/xmrig_manager.cpp | 11 ++- src/rpc/rpc_client.cpp | 28 +++++- src/ui/windows/export_all_keys_dialog.cpp | 11 +-- src/ui/windows/export_transactions_dialog.cpp | 21 +++-- src/util/bootstrap.cpp | 35 +++++++ src/util/http_download.cpp | 14 ++- src/util/xmrig_updater.cpp | 6 ++ src/wallet/lite_connection_service.cpp | 48 +++++++++- src/wallet/lite_wallet_controller.cpp | 22 ++++- 18 files changed, 289 insertions(+), 155 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9440393..58ffce8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,6 @@ set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported) set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version") set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory") set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest") -option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF) set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS litelib_wallet_exists litelib_initialize_new @@ -126,31 +125,12 @@ if(DRAGONX_ENABLE_LITE_BACKEND) if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}") message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}") endif() - if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE) - if(NOT DRAGONX_LITE_BACKEND_MANIFEST) - message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST") - endif() - file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON) - string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status) - if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR) - message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status") - endif() - if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified") - message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata") - endif() - string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256) - string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256) - if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR) - message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata") - endif() - if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA) - message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256") - endif() - string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed) - if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED) - message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true") - endif() - endif() + # Note (F15-1): the former signature-metadata gate was removed. It trusted a + # "verification_status: verified" field that scripts/build-lite-backend-artifact.sh + # self-attested with no cryptographic check (the "verified" SHA was just the artifact's + # own SHA). The trust root is now build-from-source: that script builds the backend from + # the vendored in-tree source and refuses prebuilt artifacts, so the library linked here + # is the one built from reviewed source. The required-symbol inventory check above stays. add_library(dragonx_lite_backend UNKNOWN IMPORTED) set_target_properties(dragonx_lite_backend PROPERTIES @@ -1204,5 +1184,5 @@ message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}") message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}") message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}") message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}") -message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}") +message(STATUS " Lite trust: built-from-source (vendored third_party/silentdragonxlite)") message(STATUS "") diff --git a/build.sh b/build.sh index 8083281..dc81075 100755 --- a/build.sh +++ b/build.sh @@ -478,18 +478,28 @@ APPRUN done [[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true - # appimagetool + # appimagetool — pinned to a tagged release and SHA-256 verified before we exec it. + # The old "continuous" tag is a MOVING build fetched over the network and run on the release + # builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package. + local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage" + local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1" local APPIMAGETOOL="" if command -v appimagetool &>/dev/null; then - APPIMAGETOOL="appimagetool" - elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then - APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage" + APPIMAGETOOL="appimagetool" # maintainer's own trusted system install else - info "Downloading appimagetool ..." - wget -q -O "$bd/appimagetool-x86_64.AppImage" \ - "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x "$bd/appimagetool-x86_64.AppImage" - APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage" + local at="$bd/appimagetool-x86_64.AppImage" + # Re-verify any cached copy too; a stale unverified download must not be trusted. + if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then + info "Downloading appimagetool 1.9.0 (pinned) ..." + wget -q -O "$at" "$APPIMAGETOOL_URL" + if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then + err "appimagetool SHA-256 verification failed — refusing to use it" + rm -f "$at" + return 1 + fi + chmod +x "$at" + fi + APPIMAGETOOL="$at" fi local ARCH @@ -1311,3 +1321,9 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then [[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip" [[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg" fi + +# Reaching here means the build completed (real failures exit 1 at their point of failure). +# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a +# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise +# become the script's exit code and make a successful build report failure (e.g. to CI). +exit 0 diff --git a/scripts/build-lite-backend-artifact.sh b/scripts/build-lite-backend-artifact.sh index e83b021..66292c1 100755 --- a/scripts/build-lite-backend-artifact.sh +++ b/scripts/build-lite-backend-artifact.sh @@ -67,25 +67,9 @@ Options: --backend-dir PATH SilentDragonXLite/lib source directory. --silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path. --out-dir PATH Output directory for copied artifact and metadata. - --artifact PATH Inventory an existing artifact instead of building. - --no-build Do not run cargo; requires --artifact. --reproducible Add deterministic Rust path remaps for clean builds. --remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible. --builder NAME Redacted builder/provenance label. Default: local. - --signature-required Fail if verified signature metadata is not supplied. - --signature-file PATH Existing sidecar signature file to record. - --signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other. - --signature-verification-tool T Verification tool and version used by the release builder. - --signature-verification-command C - Verification command already run by the release builder. - --signature-key-fingerprint F Reviewed public-key fingerprint, when applicable. - --signature-certificate-identity ID - Reviewed certificate identity, when applicable. - --signature-certificate-issuer I - Reviewed certificate issuer, when applicable. - --signature-transparency-log-url URL - Transparency log entry, when applicable. - --signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check. -j, --jobs N Cargo parallel jobs. --cargo-arg ARG Extra argument forwarded to cargo build. -h, --help Show this help. @@ -95,9 +79,13 @@ Outputs: //lite-backend-symbols.txt //lite-backend-artifact-manifest.json -The script captures symbols, checksums, and optional read-only signature -verification metadata only. It does not load the library, resolve function -pointers, call SDXL, sign, upload, or publish artifacts. +The lite backend is always built from the vendored in-tree source +(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts +and self-attested signature metadata are NOT accepted (F15-1) — the previous +scheme only recorded an unverified "verified" claim. The script captures the +freshly-built artifact's symbols and checksum, and records build provenance. +It does not load the library, resolve function pointers, call SDXL, sign, +upload, or publish artifacts. EOF } @@ -166,15 +154,8 @@ while [[ $# -gt 0 ]]; do OUT_DIR="$(absolute_path "$2")" shift 2 ;; - --artifact) - [[ $# -ge 2 ]] || die "--artifact requires a value" - ARTIFACT_PATH="$(absolute_path "$2")" - BUILD_ARTIFACT=false - shift 2 - ;; - --no-build) - BUILD_ARTIFACT=false - shift + --artifact|--no-build) + die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted." ;; --reproducible) REPRODUCIBLE=true @@ -191,54 +172,11 @@ while [[ $# -gt 0 ]]; do BUILDER="$2" shift 2 ;; - --signature-required) - SIGNATURE_REQUIRED=true - shift - ;; - --signature-file|--signature-path) - [[ $# -ge 2 ]] || die "$1 requires a value" - SIGNATURE_FILE="$(absolute_path "$2")" - shift 2 - ;; - --signature-format) - [[ $# -ge 2 ]] || die "--signature-format requires a value" - SIGNATURE_FORMAT="$2" - shift 2 - ;; - --signature-verification-tool|--signature-tool) - [[ $# -ge 2 ]] || die "$1 requires a value" - SIGNATURE_VERIFICATION_TOOL="$2" - shift 2 - ;; - --signature-verification-command) - [[ $# -ge 2 ]] || die "--signature-verification-command requires a value" - SIGNATURE_VERIFICATION_COMMAND="$2" - shift 2 - ;; - --signature-key-fingerprint) - [[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value" - SIGNATURE_KEY_FINGERPRINT="$2" - shift 2 - ;; - --signature-certificate-identity) - [[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value" - SIGNATURE_CERTIFICATE_IDENTITY="$2" - shift 2 - ;; - --signature-certificate-issuer) - [[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value" - SIGNATURE_CERTIFICATE_ISSUER="$2" - shift 2 - ;; - --signature-transparency-log-url) - [[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value" - SIGNATURE_TRANSPARENCY_LOG_URL="$2" - shift 2 - ;; - --signature-verified-sha256) - [[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value" - SIGNATURE_VERIFIED_SHA256="$2" - shift 2 + --signature-required|--signature-file|--signature-path|--signature-format|\ +--signature-verification-tool|--signature-tool|--signature-verification-command|\ +--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\ +--signature-transparency-log-url|--signature-verified-sha256) + die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root." ;; -j|--jobs) [[ $# -ge 2 ]] || die "--jobs requires a value" @@ -766,6 +704,7 @@ MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json" printf ' },\n' printf ' "provenance": {\n' printf ' "owner_ready": true,\n' + printf ' "built_from_source": true,\n' printf ' "metadata_provided": true,\n' printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n' printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n' diff --git a/scripts/create-appimage.sh b/scripts/create-appimage.sh index 794a482..5e28faf 100755 --- a/scripts/create-appimage.sh +++ b/scripts/create-appimage.sh @@ -24,18 +24,27 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then exit 1 fi -# Check for appimagetool +# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it. +# The old "continuous" tag is a moving, unverified network download that runs on the release +# builder; verify it or refuse to package. +APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage" +APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1" APPIMAGETOOL="" if command -v appimagetool &> /dev/null; then - APPIMAGETOOL="appimagetool" -elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then - APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage" + APPIMAGETOOL="appimagetool" # maintainer's own trusted system install else - print_status "Downloading appimagetool..." - wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \ - "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage" - APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage" + AT="${BUILD_DIR}/appimagetool-x86_64.AppImage" + if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then + print_status "Downloading appimagetool 1.9.0 (pinned)..." + wget -q -O "$AT" "$APPIMAGETOOL_URL" + if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then + print_error "appimagetool SHA-256 verification failed — refusing to use it" + rm -f "$AT" + exit 1 + fi + chmod +x "$AT" + fi + APPIMAGETOOL="$AT" fi print_status "Creating AppDir structure..." diff --git a/setup.sh b/setup.sh index 2340bee..dd20983 100755 --- a/setup.sh +++ b/setup.sh @@ -391,11 +391,26 @@ elif $SETUP_SAPLING; then SPEND_URL="https://z.cash/downloads/sapling-spend.params" OUTPUT_URL="https://z.cash/downloads/sapling-output.params" + # Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every + # Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is + # plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file. + SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13" + OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4" - curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \ - ok "Downloaded sapling-spend.params" - curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \ - ok "Downloaded sapling-output.params" + if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \ + && echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then + ok "Downloaded + verified sapling-spend.params" + else + rm -f "$PARAMS_DIR/sapling-spend.params" + err "sapling-spend.params download or SHA-256 verification failed — not installed" + fi + if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \ + && echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then + ok "Downloaded + verified sapling-output.params" + else + rm -f "$PARAMS_DIR/sapling-output.params" + err "sapling-output.params download or SHA-256 verification failed — not installed" + fi fi else skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)" diff --git a/src/app.cpp b/src/app.cpp index 06efcf9..9258b5d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -5320,6 +5320,11 @@ void App::renderLoadingOverlay(float contentH) void App::shutdown() { + // Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer + // never fires if the user quits sooner, which would otherwise leave a key/seed resident. + // (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().) + clearSecretClipboardIfArmed(); + // Clean up bootstrap if running if (bootstrap_) { bootstrap_->cancel(); @@ -5522,10 +5527,9 @@ void App::copySecretToClipboard(const std::string& secret) ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f); } -void App::pumpSecretClipboardClear() +void App::clearSecretClipboardIfArmed() { - if (clipboard_clear_deadline_ <= 0.0) return; - if (ImGui::GetTime() < clipboard_clear_deadline_) return; + if (clipboard_secret_hash_ == 0) return; // Only clear if the clipboard STILL holds our secret (the user may have copied something else). if (const char* cb = ImGui::GetClipboardText()) { std::uint64_t h = 1469598103934665603ULL; @@ -5536,6 +5540,13 @@ void App::pumpSecretClipboardClear() clipboard_secret_hash_ = 0; } +void App::pumpSecretClipboardClear() +{ + if (clipboard_clear_deadline_ <= 0.0) return; + if (ImGui::GetTime() < clipboard_clear_deadline_) return; + clearSecretClipboardIfArmed(); +} + void App::maybeFinishTransactionSendProgress() { using Job = services::NetworkRefreshService::Job; diff --git a/src/app.h b/src/app.h index a762b8a..9dea83e 100644 --- a/src/app.h +++ b/src/app.h @@ -573,6 +573,9 @@ public: // plaintext. Call pumpSecretClipboardClear() each frame to action the clear. void copySecretToClipboard(const std::string& secret); void pumpSecretClipboardClear(); + // Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer). + // Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard. + void clearSecretClipboardIfArmed(); bool isTransactionRefreshInProgress() const { return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions); } diff --git a/src/chat/chat_database.cpp b/src/chat/chat_database.cpp index 3aac67e..13d697f 100644 --- a/src/chat/chat_database.cpp +++ b/src/chat/chat_database.cpp @@ -228,6 +228,17 @@ bool ChatDatabase::ensureOpen() exec("PRAGMA journal_mode=WAL"); exec("PRAGMA synchronous=NORMAL"); + // C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with + // umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD + // ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows). + { + std::error_code perr; + const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; + std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr); + std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr); + std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr); + } + if (!createSchema()) { close(); return false; diff --git a/src/daemon/seed_wallet_creator.cpp b/src/daemon/seed_wallet_creator.cpp index 05bbc57..8d3349b 100644 --- a/src/daemon/seed_wallet_creator.cpp +++ b/src/daemon/seed_wallet_creator.cpp @@ -136,6 +136,14 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir, } } + // W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a + // later step failed (empty address, or z_getnewaddress threw), the caller discards this + // result without wiping it, which would leave the seed resident. Success keeps it deliberately. + if (!r.ok && !r.seedPhrase.empty()) { + sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size()); + r.seedPhrase.clear(); + } + // 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly). cli.disconnect(); temp.stop(20000); diff --git a/src/daemon/xmrig_manager.cpp b/src/daemon/xmrig_manager.cpp index f2e5c0e..9177233 100644 --- a/src/daemon/xmrig_manager.cpp +++ b/src/daemon/xmrig_manager.cpp @@ -208,19 +208,20 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath) try { fs::create_directories(fs::path(outPath).parent_path()); - std::ofstream ofs(outPath); + std::ofstream ofs(outPath, std::ios::trunc); if (!ofs.is_open()) { last_error_ = "Cannot write xmrig config: " + outPath; DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); return false; } - ofs << j.dump(4); - ofs.close(); - #ifndef _WIN32 - // 0600 permissions — only owner can read/write + // Restrict to owner (0600) BEFORE writing any secret material (API token, wallet + // address, worker name). The file is still empty here, so the config is never + // world-readable — closing the window between creation and the previous post-write chmod. chmod(outPath.c_str(), 0600); #endif + ofs << j.dump(4); + ofs.close(); return true; } catch (const std::exception& e) { last_error_ = std::string("Config write error: ") + e.what(); diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index 29f0d5f..a1e8c04 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -25,9 +25,11 @@ namespace { // Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree // that held a secret (B7). Operates on the underlying std::string buffers via get_ref. -void scrubJsonSecrets(nlohmann::json& j) { +// Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter). +template +void scrubJsonSecrets(J& j) { if (j.is_string()) { - auto& s = j.get_ref(); + auto& s = j.template get_ref(); if (!s.empty()) sodium_memzero(&s[0], s.size()); } else if (j.is_object() || j.is_array()) { for (auto& el : j) scrubJsonSecrets(el); @@ -96,6 +98,10 @@ void RPCClient::setTraceSource(std::string source) // Callback for libcurl to write response data static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) { size_t totalSize = size * nmemb; + // Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded + // response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion. + static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024; + if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0; // short count aborts the transfer userp->append((char*)contents, totalSize); return totalSize; } @@ -202,6 +208,10 @@ bool RPCClient::connect(const std::string& host, const std::string& port, // budget for the TCP + TLS handshake over real network latency (1s would spuriously fail). const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L; curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout); + // Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's + // build defaults. Harmless on the localhost http:// case; essential for a remote https daemon. + curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L); // Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy // local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long @@ -508,14 +518,22 @@ std::string RPCClient::callRaw(const std::string& method, const json& params) } auto& result = oj["result"]; + std::string out; if (result.is_null()) { - return "null"; + out = "null"; } else if (result.is_string()) { // Return the raw string (not JSON-encoded) — caller wraps as needed - return result.get(); + out = result.get(); } else { - return result.dump(4); + out = result.dump(4); } + // B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey, + // whose response carries plaintext key material. Zero the raw buffer and the parsed tree so + // the secret does not linger in freed heap (matching callSecret). The single returned copy is + // the caller's to manage. + scrubJsonSecrets(oj); + if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size()); + return out; } void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err) diff --git a/src/ui/windows/export_all_keys_dialog.cpp b/src/ui/windows/export_all_keys_dialog.cpp index 699418c..cdb3c98 100644 --- a/src/ui/windows/export_all_keys_dialog.cpp +++ b/src/ui/windows/export_all_keys_dialog.cpp @@ -211,12 +211,11 @@ void ExportAllKeysDialog::render(App* app) std::string filepath = configDir + "/" + filename; bool writeOk = false; if (exported > 0) { - std::ofstream file(filepath); - if (file.is_open()) { - file << keys; - file.close(); - writeOk = true; - } + // Write the plaintext private keys 0600 + atomically (never + // world/group-readable, never a half-written file) — the same restricted + // atomic-write idiom the PIN vault uses. A default ofstream would create + // the key dump with umask-derived (often 0644) permissions. + writeOk = util::Platform::writeFileAtomically(filepath, keys, /*restrictPermissions=*/true); } if (!keys.empty()) sodium_memzero(&keys[0], keys.size()); // don't leave every key in freed heap diff --git a/src/ui/windows/export_transactions_dialog.cpp b/src/ui/windows/export_transactions_dialog.cpp index 1c51555..b391dca 100644 --- a/src/ui/windows/export_transactions_dialog.cpp +++ b/src/ui/windows/export_transactions_dialog.cpp @@ -36,21 +36,30 @@ static bool s_exporting = false; // Helper to escape CSV field static std::string escapeCSV(const std::string& field) { - if (field.find(',') != std::string::npos || - field.find('"') != std::string::npos || - field.find('\n') != std::string::npos) { + // Neutralize spreadsheet formula injection: a field beginning with '=', '+', '-', '@', + // tab, or CR is interpreted as a formula by Excel/LibreOffice, letting an attacker-supplied + // memo/address execute on open. Prefix such fields with a single quote so they render as text. + std::string safe = field; + if (!safe.empty()) { + const char c0 = safe.front(); + if (c0 == '=' || c0 == '+' || c0 == '-' || c0 == '@' || c0 == '\t' || c0 == '\r') + safe.insert(safe.begin(), '\''); + } + if (safe.find(',') != std::string::npos || + safe.find('"') != std::string::npos || + safe.find('\n') != std::string::npos) { // Escape quotes and wrap in quotes std::string escaped; - escaped.reserve(field.size() + 4); + escaped.reserve(safe.size() + 4); escaped += '"'; - for (char c : field) { + for (char c : safe) { if (c == '"') escaped += "\"\""; else escaped += c; } escaped += '"'; return escaped; } - return field; + return safe; } void ExportTransactionsDialog::show() diff --git a/src/util/bootstrap.cpp b/src/util/bootstrap.cpp index ec02456..a8491c6 100644 --- a/src/util/bootstrap.cpp +++ b/src/util/bootstrap.cpp @@ -31,6 +31,31 @@ static bool endsWith(const std::string& s, const std::string& suffix) { return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; } +// Reject archive member names that would escape the extraction root (zip-slip). +// The snapshot legitimately carries sub-paths (blocks/, chainstate/), so — unlike the +// updaters, which flatten to baseName() — we keep the relative path but refuse any entry +// that is absolute, drive/UNC-rooted, or contains a ".." component. +static bool isSafeArchivePath(const std::string& name) { + if (name.empty()) return false; + std::string n = name; + std::replace(n.begin(), n.end(), '\\', '/'); // normalize Windows separators + if (n.front() == '/') return false; // absolute POSIX path + const char c0 = n[0]; + if (n.size() >= 2 && n[1] == ':' && + ((c0 >= 'A' && c0 <= 'Z') || (c0 >= 'a' && c0 <= 'z'))) + return false; // Windows drive letter (C:...) + size_t start = 0; + while (start <= n.size()) { + const size_t slash = n.find('/', start); + const std::string comp = + n.substr(start, slash == std::string::npos ? std::string::npos : slash - start); + if (comp == "..") return false; // path traversal component + if (slash == std::string::npos) break; + start = slash + 1; + } + return true; +} + static size_t writeFileCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t total = size * nmemb; FILE* fp = static_cast(userp); @@ -383,6 +408,16 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir) std::string filename = stat.m_filename; + // *** SECURITY: reject zip-slip / path-traversal entries before building any path *** + // A legitimate snapshot from the project host never contains these; an entry that does + // indicates a malicious/corrupt archive, so abort rather than silently skip. + if (!isSafeArchivePath(filename)) { + DEBUG_LOGF("[Bootstrap] Unsafe archive path rejected: %s\n", filename.c_str()); + setProgress(State::Failed, "Refusing to extract unsafe archive entry: " + filename); + mz_zip_reader_end(&zip); + return false; + } + // *** CRITICAL: Skip wallet.dat *** if (filename == "wallet.dat" || endsWith(filename, "/wallet.dat")) { DEBUG_LOGF("[Bootstrap] Skipping wallet.dat (protected)\n"); diff --git a/src/util/http_download.cpp b/src/util/http_download.cpp index 5fe7ba1..1f3af2a 100644 --- a/src/util/http_download.cpp +++ b/src/util/http_download.cpp @@ -19,10 +19,19 @@ namespace util { namespace { +// Cap for in-memory text/JSON responses (release metadata, price/candle data). Far above any +// legitimate body, but bounds memory if a hostile/MITM'd server streams an unbounded response. +constexpr std::size_t kMaxMetadataBytes = 16u * 1024 * 1024; + size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp) { - static_cast(userp)->append(static_cast(contents), size * nmemb); - return size * nmemb; + auto* s = static_cast(userp); + const size_t n = size * nmemb; + // Hard cap: a chunked response omits Content-Length, so CURLOPT_MAXFILESIZE cannot catch it — + // aborting here (short count) makes curl fail the transfer instead of exhausting memory. + if (s->size() + n > kMaxMetadataBytes) return 0; + s->append(static_cast(contents), n); + return n; } size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp) @@ -52,6 +61,7 @@ std::string httpGetString(const std::string& url, const char* logTag) curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast(kMaxMetadataBytes)); // reject oversized metadata curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); const CURLcode res = curl_easy_perform(curl); diff --git a/src/util/xmrig_updater.cpp b/src/util/xmrig_updater.cpp index d9c8d6c..25e8343 100644 --- a/src/util/xmrig_updater.cpp +++ b/src/util/xmrig_updater.cpp @@ -314,6 +314,12 @@ void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRele return; } } + } else if (kXmrigRequireSignature) { + // Signatures are required but no key is pinned in this build: fail closed rather than + // silently downgrading to checksum-only (the checksum is same-origin as the archive). + fs::remove(zipPath, ec); + setProgress(State::Failed, "No signing key is pinned in this build — refusing to install."); + return; } } diff --git a/src/wallet/lite_connection_service.cpp b/src/wallet/lite_connection_service.cpp index 6133122..1c444de 100644 --- a/src/wallet/lite_connection_service.cpp +++ b/src/wallet/lite_connection_service.cpp @@ -81,10 +81,56 @@ LiteConnectionSettings defaultLiteConnectionSettings() return settings; } +// Strict dotted-decimal check for the 127.0.0.0/8 loopback block: exactly four numeric octets +// (each 0-255) with the first equal to 127. This must NOT be a prefix match — "127.0.0.1.evil.com" +// and "127.evil.com" are attacker-controlled DNS names that a startsWith("127.") test would wrongly +// treat as loopback, reopening the plaintext-downgrade hole. +static bool isNumericIpv4Loopback(const std::string& h) +{ + int parts = 0; + size_t start = 0; + while (true) { + const size_t dot = h.find('.', start); + const std::string seg = h.substr(start, dot == std::string::npos ? std::string::npos : dot - start); + if (seg.empty() || seg.size() > 3) return false; + int v = 0; + for (char c : seg) { if (c < '0' || c > '9') return false; v = v * 10 + (c - '0'); } + if (v > 255) return false; + if (parts == 0 && v != 127) return false; // 127.0.0.0/8 only + ++parts; + if (dot == std::string::npos) break; + start = dot + 1; + } + return parts == 4; +} + +static bool isLoopbackLiteHostSpec(const std::string& hostPort) +{ + std::string h = hostPort; + const size_t term = h.find_first_of("/?#"); // strip path/query/fragment + if (term != std::string::npos) h = h.substr(0, term); + const size_t at = h.rfind('@'); // strip userinfo (user:pass@host) + if (at != std::string::npos) h = h.substr(at + 1); + if (!h.empty() && h.front() == '[') { // [::1]:port (bracketed IPv6) + const size_t close = h.find(']'); + h = (close == std::string::npos) ? h : h.substr(1, close - 1); + } else { // host or host:port + const size_t colon = h.find(':'); + if (colon != std::string::npos) h = h.substr(0, colon); + } + return h == "localhost" || h == "::1" || isNumericIpv4Loopback(h); +} + bool isLiteServerUrlUsable(const std::string& serverUrl) { const std::string normalized = liteTrimCopy(serverUrl); - return startsWith(normalized, "https://") || startsWith(normalized, "http://"); + if (startsWith(normalized, "https://")) return true; + // SECURITY: plaintext http:// is a TLS downgrade for lightwalletd traffic (view keys, + // transactions, addresses). Permit it only for loopback (a local dev lightwalletd); + // reject remote plaintext servers instead of silently accepting the downgrade. + if (startsWith(normalized, "http://")) + return isLoopbackLiteHostSpec(normalized.substr(sizeof("http://") - 1)); + return false; } bool isOfficialLiteServer(const std::string& serverUrl) diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index 6d860d3..839781c 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -1066,7 +1066,16 @@ LiteEncryptionResult LiteWalletController::encryptWallet(std::string passphrase) } out = parseEncryptionOpResponse(bridge_->execute("encrypt", passphrase)); secureWipeLiteSecret(passphrase); - if (out.ok) bridge_->execute("save", ""); // persist the now-encrypted wallet + if (out.ok) { + // Persist the now-encrypted wallet. If the save fails, do NOT report success — the + // on-disk wallet would still be unencrypted, contradicting what the user was told. + const auto saved = bridge_->execute("save", ""); + if (!saved.ok) { + out.ok = false; + out.error = "wallet encrypted in memory but saving to disk failed" + + (saved.error.empty() ? std::string() : (": " + saved.error)); + } + } return out; } @@ -1084,7 +1093,16 @@ LiteEncryptionResult LiteWalletController::decryptWallet(std::string passphrase) } out = parseEncryptionOpResponse(bridge_->execute("decrypt", passphrase)); secureWipeLiteSecret(passphrase); - if (out.ok) bridge_->execute("save", ""); // persist the now-unencrypted wallet + if (out.ok) { + // Persist the now-unencrypted wallet. If the save fails, do NOT report success — the + // on-disk wallet would still be encrypted, contradicting what the user was told. + const auto saved = bridge_->execute("save", ""); + if (!saved.ok) { + out.ok = false; + out.error = "wallet decrypted in memory but saving to disk failed" + + (saved.error.empty() ? std::string() : (": " + saved.error)); + } + } return out; } From c53b7f771e1d2151817c94fb58ff73c37a297141 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 22 Jul 2026 16:01:51 -0500 Subject: [PATCH 2/2] fix(ui): overview/mining polish + default-pool cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - balance: inset the address-row favorite (star) button by the card's inner padding so it mirrors the left margin instead of hugging the card edge. - mining: remove pool.dragonx.cc from the built-in default pools (pool.dragonx.is is now the sole default); update the pool-registry test accordingly. - mining: middle-truncate saved pool-URL and payout-address dropdown rows (new shared material::TruncateToWidth helper) so a full z-address no longer runs under the trailing delete (X) button. - mining: fix the thread-grid cells overflowing the card at >100% display scaling — the reserved Mine-button width used a raw clamp that didn't scale; scale it by dp so cols is estimated correctly (no-op at 100%). Full-node build + test suite green. Co-Authored-By: Claude Fable 5 --- src/ui/material/draw_helpers.h | 15 ++++++++++++++ src/ui/windows/balance_address_list.cpp | 5 ++++- src/ui/windows/mining_controls.cpp | 5 ++++- src/ui/windows/mining_mode_toggle.cpp | 14 +++++++++---- src/util/pool_registry_core.cpp | 8 -------- tests/test_phase4.cpp | 27 ++++++++++++------------- 6 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 422d541..0111650 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -57,6 +57,21 @@ inline ImU32 ReadableError() { return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF); } +// Middle-ellipsis truncation ("front...back", roughly equal halves) so `text` fits within +// maxWidth pixels when drawn with `font` at `fontSize`. Returns `text` unchanged if it already +// fits (or maxWidth is non-positive). Display-only — never mutate the underlying value with this. +inline std::string TruncateToWidth(const std::string& text, ImFont* font, float fontSize, float maxWidth) { + if (text.empty() || !font || maxWidth <= 0.0f) return text; + if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, text.c_str()).x <= maxWidth) return text; + const int n = static_cast(text.size()); + for (int f = n / 2; f >= 3; --f) { + const int b = (f - 2 > 3) ? (f - 2) : 3; // keep the two halves roughly equal + std::string t = text.substr(0, f) + "..." + text.substr(n - b); + if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, t.c_str()).x <= maxWidth) return t; + } + return n > 6 ? (text.substr(0, 3) + "..." + text.substr(n - 3)) : text; +} + // Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase. inline const char* LoadingDots() { int n = ((int)(ImGui::GetTime() * 3.0f)) % 4; diff --git a/src/ui/windows/balance_address_list.cpp b/src/ui/windows/balance_address_list.cpp index 818c2e6..937baec 100644 --- a/src/ui/windows/balance_address_list.cpp +++ b/src/ui/windows/balance_address_list.cpp @@ -70,6 +70,7 @@ AddressRowLayout ComputeAddressRowLayout(float rowX, float spacingSm, float spacingXs) { + (void)spacingXs; // trailing button now insets by rowPadLeft (mirrors the left margin) AddressRowLayout layout; layout.contentStartX = rowX + rowPadLeft; layout.contentStartY = rowY + spacingMd; @@ -77,7 +78,9 @@ AddressRowLayout ComputeAddressRowLayout(float rowX, const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f; const float rightEdge = rowX + rowWidth; - const float favoriteX = rightEdge - layout.buttonSize - spacingXs; + // Inset the trailing (favorite/star) button by the card's inner padding — the same margin the + // left content uses (rowPadLeft) — so it mirrors the left edge instead of hugging the card edge. + const float favoriteX = rightEdge - layout.buttonSize - rowPadLeft; const float visibilityX = favoriteX - spacingSm - layout.buttonSize; layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize}; diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index 009fd02..133db30 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -57,7 +57,10 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& // --- Compute thread grid layout based on controls card width --- // Estimate controlsW first to compute cols correctly - float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap; + // The Mine button is square (= card height, which scales with DPI), so the width we + // reserve for it here must scale too — a RAW clamp under-reserves at >100% scaling, which + // over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%). + float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap; float innerW = estControlsW - pad * 2; float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f)); float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size); diff --git a/src/ui/windows/mining_mode_toggle.cpp b/src/ui/windows/mining_mode_toggle.cpp index bad367e..862fd36 100644 --- a/src/ui/windows/mining_mode_toggle.cpp +++ b/src/ui/windows/mining_mode_toggle.cpp @@ -308,11 +308,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); if (rowHov && !inXZone) pdl->AddRectFilled(rowMin, rowMax, StateHover()); - // Item text with internal padding + // Item text with internal padding, middle-truncated so a long saved URL + // can't run under the trailing X (delete) button. float textY = rowMin.y + (rowH - rowFontSz) * 0.5f; + float maxTextW = popupInnerW - xZoneW - textPadX * 2.0f; + std::string urlDisp = material::TruncateToWidth(url, rowFont, rowFontSz, maxTextW); pdl->AddText(rowFont, rowFontSz, ImVec2(rowMin.x + textPadX, textY), - isCurrent ? Primary() : OnSurface(), url.c_str()); + isCurrent ? Primary() : OnSurface(), urlDisp.c_str()); // X button — flush with right edge, icon centered { ImVec2 xMin(rowMax.x - xZoneW, rowMin.y); @@ -467,11 +470,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); if (rowHov && !inXZone) pdl->AddRectFilled(rowMin, rowMax, StateHover()); - // Full address text with internal padding + // Address text with internal padding, middle-truncated so a full z-address + // (~78 chars) can't run under the trailing X (delete) button. float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f; + float wMaxTextW = wPopupInnerW - wXZoneW - wTextPadX * 2.0f; + std::string addrDisp = material::TruncateToWidth(addr, wRowFont, wRowFontSz, wMaxTextW); pdl->AddText(wRowFont, wRowFontSz, ImVec2(rowMin.x + wTextPadX, textY), - isCurrent ? Primary() : OnSurface(), addr.c_str()); + isCurrent ? Primary() : OnSurface(), addrDisp.c_str()); // Tooltip for long addresses if (rowHov && !inXZone) material::Tooltip("%s", addr.c_str()); diff --git a/src/util/pool_registry_core.cpp b/src/util/pool_registry_core.cpp index e6cfcc7..619cd76 100644 --- a/src/util/pool_registry_core.cpp +++ b/src/util/pool_registry_core.cpp @@ -30,14 +30,6 @@ const std::vector& knownPools() "https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs, /*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true, }, - KnownPool{ - // The mining (stratum) host is us.dragonx.cc — pool.dragonx.cc is the - // Cloudflare-proxied web/API host and does NOT accept stratum on :3333. - // Stats still come from pool.dragonx.cc/api/pools (proxied HTTP is fine). - "dragonx-cc-pplns", "pool.dragonx.cc", "us.dragonx.cc:3333", "rx/dragonx", - "https://pool.dragonx.cc/api/pools", PoolStatsSchema::Miningcore, - /*miningcorePoolId=*/"dragonx-pplns", /*feePercent=*/3.0, /*official=*/true, - }, }; return pools; } diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index cd826f5..6925049 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -3434,9 +3434,11 @@ void testBalanceAddressListModel() EXPECT_NEAR(layout.contentStartX, 22.0, 0.0001); EXPECT_NEAR(layout.contentStartY, 26.0, 0.0001); EXPECT_NEAR(layout.buttonSize, 38.0, 0.0001); - EXPECT_NEAR(layout.favoriteButton.x, 270.0, 0.0001); - EXPECT_NEAR(layout.visibilityButton.x, 228.0, 0.0001); - EXPECT_NEAR(layout.contentRight, 224.0, 0.0001); + // Trailing (favorite) button is inset by rowPadLeft (12) so it mirrors the left margin + // instead of hugging the card edge: 310 - 38 - 12 = 260. + EXPECT_NEAR(layout.favoriteButton.x, 260.0, 0.0001); + EXPECT_NEAR(layout.visibilityButton.x, 218.0, 0.0001); + EXPECT_NEAR(layout.contentRight, 214.0, 0.0001); EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(2.0, 3.5), std::string("$7.00")); EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.001, 2.0), std::string("$0.002000")); EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string("")); @@ -5774,23 +5776,20 @@ void testXmrigLiveInstall() void testPoolRegistryLookup() { using namespace dragonx::util; - EXPECT_EQ(knownPools().size(), static_cast(2)); + // pool.dragonx.cc was removed from the built-in defaults; pool.dragonx.is is the sole entry. + EXPECT_EQ(knownPools().size(), static_cast(1)); - // The algo follows the pool. Note pool.dragonx.cc's stratum host is us.dragonx.cc - // (the .cc domain is only the Cloudflare-proxied web/API host). - EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/dragonx")); + // The algo follows the pool: pool.dragonx.is resolves to rx/hush regardless of the caller default. EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush")); - // Bare host (no port) still matches; scheme + path are tolerated. - EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc", "rx/hush"), std::string("rx/dragonx")); - EXPECT_EQ(resolvePoolAlgo("stratum+tcp://us.dragonx.cc:3333/x", "rx/hush"), - std::string("rx/dragonx")); - // Unknown host -> fallback algo. + // The former us.dragonx.cc (pool.dragonx.cc) host is now unknown -> caller's fallback algo. + EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/hush")); EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush")); - EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") != nullptr); + EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:3433") != nullptr); + EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") == nullptr); // removed built-in default EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr); // A mismatched explicit port must NOT match a known pool. - EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:9999") == nullptr); + EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:9999") == nullptr); } // Schema-aware pool hashrate parsing (the two pools speak different APIs).