fix(storage): owner-only secret files + bound SQLite cache growth
- Write vault.dat atomically and 0600 (it holds the PIN-encrypted passphrase, so a world-readable copy enables an offline brute-force of the short PIN), and chmod the tx-history SQLite + its WAL/SHM sidecars to 0600 on open. - The tx-history snapshot and key-salt rows are keyed on a hash of the full address set, which changes whenever a new address is generated — orphaning the prior hash's full-history blob and salt forever. pruneOtherWallets() now drops rows for every non-live wallet hash on each save, bounding the database. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,10 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@@ -194,6 +198,14 @@ bool TransactionHistoryCache::ensureOpen()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Owner-only (0600): although the payload is encrypted, don't leave the cache (or its
|
||||||
|
// WAL/SHM sidecars) world-readable. Best-effort; sidecars may not exist until first write.
|
||||||
|
::chmod(database_path_.c_str(), 0600);
|
||||||
|
::chmod((database_path_ + "-wal").c_str(), 0600);
|
||||||
|
::chmod((database_path_ + "-shm").c_str(), 0600);
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +344,9 @@ bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
|||||||
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
||||||
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
||||||
|
|
||||||
return sqlite3_step(statement.handle) == SQLITE_DONE;
|
if (sqlite3_step(statement.handle) != SQLITE_DONE) return false;
|
||||||
|
pruneOtherWallets(walletHash); // bound DB growth — drop stale-hash snapshots/salts
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
||||||
@@ -499,6 +513,20 @@ void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
|
|||||||
sqlite3_step(statement.handle);
|
sqlite3_step(statement.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::pruneOtherWallets(const std::string& keepWalletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen() || keepWalletHash.empty()) return;
|
||||||
|
// Table names are hardcoded literals (no injection surface). Prune both the snapshot
|
||||||
|
// blobs and the now-orphaned salt rows so a stale salt can't outlive its ciphertext.
|
||||||
|
for (const char* table : {"transaction_history_snapshots", "transaction_history_keys"}) {
|
||||||
|
const std::string sql = std::string("DELETE FROM ") + table + " WHERE wallet_hash <> ?";
|
||||||
|
Statement statement(db_, sql.c_str());
|
||||||
|
if (!statement.handle) continue;
|
||||||
|
if (!bindText(statement.handle, 1, keepWalletHash)) continue;
|
||||||
|
sqlite3_step(statement.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TransactionHistoryCache::close()
|
void TransactionHistoryCache::close()
|
||||||
{
|
{
|
||||||
if (!db_) return;
|
if (!db_) return;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ private:
|
|||||||
std::vector<unsigned char>& nonce,
|
std::vector<unsigned char>& nonce,
|
||||||
std::vector<unsigned char>& cipherText);
|
std::vector<unsigned char>& cipherText);
|
||||||
void clearWalletByHash(const std::string& walletHash);
|
void clearWalletByHash(const std::string& walletHash);
|
||||||
|
// Delete snapshot + salt rows for every wallet hash except the live one, bounding the
|
||||||
|
// DB so generating a new address (which changes the hash) doesn't orphan history forever.
|
||||||
|
void pruneOtherWallets(const std::string& keepWalletHash);
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
sqlite3* db_ = nullptr;
|
sqlite3* db_ = nullptr;
|
||||||
|
|||||||
@@ -114,25 +114,20 @@ bool SecureVault::store(const std::string& pin, const std::string& passphrase) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write vault file: version + salt + nonce + ciphertext
|
// Write vault file: version + salt + nonce + ciphertext.
|
||||||
|
// Atomic + owner-only (0600): the vault holds the PIN-encrypted passphrase, so it must
|
||||||
|
// not be world-readable (a copy enables an offline brute-force of the short PIN), and a
|
||||||
|
// crash mid-write must not leave a truncated vault.
|
||||||
std::string vaultPath = getVaultPath();
|
std::string vaultPath = getVaultPath();
|
||||||
fs::create_directories(fs::path(vaultPath).parent_path());
|
std::string content;
|
||||||
|
content.reserve(1 + sizeof(salt) + sizeof(nonce) + cipherLen);
|
||||||
|
content.push_back(static_cast<char>(VAULT_VERSION));
|
||||||
|
content.append(reinterpret_cast<const char*>(salt), sizeof(salt));
|
||||||
|
content.append(reinterpret_cast<const char*>(nonce), sizeof(nonce));
|
||||||
|
content.append(reinterpret_cast<const char*>(ciphertext.data()), cipherLen);
|
||||||
|
|
||||||
std::ofstream out(vaultPath, std::ios::binary);
|
if (!Platform::writeFileAtomically(vaultPath, content, /*restrictPermissions=*/true)) {
|
||||||
if (!out.is_open()) {
|
|
||||||
DEBUG_LOGF("[SecureVault] Cannot create vault file: %s\n", vaultPath.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
out.write(reinterpret_cast<const char*>(&VAULT_VERSION), 1);
|
|
||||||
out.write(reinterpret_cast<const char*>(salt), sizeof(salt));
|
|
||||||
out.write(reinterpret_cast<const char*>(nonce), sizeof(nonce));
|
|
||||||
out.write(reinterpret_cast<const char*>(ciphertext.data()), cipherLen);
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
if (!out.good()) {
|
|
||||||
DEBUG_LOGF("[SecureVault] Write failed\n");
|
DEBUG_LOGF("[SecureVault] Write failed\n");
|
||||||
fs::remove(vaultPath);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user