security(exposure): owner-only ACLs on Windows, auto-clearing export copy, require SHA-256 for bootstrap

- writeFileAtomically honored restrictPermissions only on POSIX (0600); on
  Windows it was a no-op, so exported keys / seed backups / the vault / chat
  exports inherited the parent dir's ACL. It now builds a PROTECTED owner-only
  DACL (current-user SID, SE_DACL_PROTECTED) on the temp file before the atomic
  rename; best-effort, falling back to default ACLs on failure. (Verified it
  compiles under mingw-w64; the POSIX path is unchanged.)
- The key-export dialog's Copy wrote the private key to the plain, permanent
  clipboard; it now goes through copySecretToClipboard() (45s armed auto-wipe +
  its own "copied, auto-clears" notice), like the seed backup copy.
- Bootstrap archive verification dropped the collision-prone MD5 fallback: it now
  requires a published SHA-256 and fails closed otherwise. (Authenticity vs a
  malicious server still needs a detached ed25519 signature like the updaters —
  a separate release-coordinated change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 10:34:47 -05:00
co-authored by Claude Opus 4.8
parent 6b0f2eda22
commit 31e9c2ee2c
3 changed files with 71 additions and 50 deletions
+22 -45
View File
@@ -1125,56 +1125,33 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
std::string expectedSha = parseChecksumFile(sha256Content, 64);
const bool haveSHA256 = !expectedSha.empty();
std::string expectedMd5;
bool haveMD5 = false;
if (!haveSHA256) { // MD5 only as a fallback when no SHA-256 is published
std::string md5Content = httpGetString(baseUrl + "/" + kZipName + ".md5", "[bootstrap]", opt);
if (cancel_requested_) return false;
expectedMd5 = parseChecksumFile(md5Content, 32);
haveMD5 = !expectedMd5.empty();
}
if (!haveSHA256 && !haveMD5) {
DEBUG_LOGF("[Bootstrap] No checksum published - refusing to install an unverified archive\n");
setProgress(State::Failed, "Verification failed: no checksum was published for the bootstrap. "
// SHA-256 is now required: the collision-prone MD5 fallback was removed. (A same-origin checksum
// only protects against transport corruption, not a malicious server — true authenticity needs a
// detached ed25519 signature like the daemon/xmrig updaters; that is a separate release-coordinated
// change, since it requires publishing per-archive .sig files and pinning a key.)
if (!haveSHA256) {
DEBUG_LOGF("[Bootstrap] No SHA-256 published - refusing to install an unverified archive\n");
setProgress(State::Failed, "Verification failed: no SHA-256 checksum was published for the bootstrap. "
"Refusing to install an unverified archive.");
return false; // fail CLOSED; archive is KEPT
}
if (haveSHA256) {
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1);
std::string actual = computeSHA256(zipPath, 0.0f, 100.0f);
if (cancel_requested_) return false;
if (actual.empty()) { setProgress(State::Failed, "SHA-256 verification error"); return false; }
if (expectedSha != actual) {
last_verify_mismatch_ = true;
DEBUG_LOGF("[Bootstrap] SHA-256 MISMATCH!\n expected: %s\n actual: %s\n",
expectedSha.c_str(), actual.c_str());
setProgress(State::Failed,
"SHA-256 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n"
"Expected: " + expectedSha.substr(0, 16) + "...\n"
"Got: " + actual.substr(0, 16) + "...\n"
"Try again or use a fresh download.");
return false;
}
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
} else { // haveMD5
setProgress(State::Verifying, "Verifying MD5...", 0, 1);
std::string actual = computeMD5(zipPath, 0.0f, 100.0f);
if (cancel_requested_) return false;
if (actual.empty()) { setProgress(State::Failed, "MD5 verification error"); return false; }
if (expectedMd5 != actual) {
last_verify_mismatch_ = true;
DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n",
expectedMd5.c_str(), actual.c_str());
setProgress(State::Failed,
"MD5 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n"
"Expected: " + expectedMd5 + "\nGot: " + actual + "\n"
"Try again or use a fresh download.");
return false;
}
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str());
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1);
std::string actual = computeSHA256(zipPath, 0.0f, 100.0f);
if (cancel_requested_) return false;
if (actual.empty()) { setProgress(State::Failed, "SHA-256 verification error"); return false; }
if (expectedSha != actual) {
last_verify_mismatch_ = true;
DEBUG_LOGF("[Bootstrap] SHA-256 MISMATCH!\n expected: %s\n actual: %s\n",
expectedSha.c_str(), actual.c_str());
setProgress(State::Failed,
"SHA-256 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n"
"Expected: " + expectedSha.substr(0, 16) + "...\n"
"Got: " + actual.substr(0, 16) + "...\n"
"Try again or use a fresh download.");
return false;
}
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93", 100.0, 100.0);
return true;