Files
ObsidianDragon/src/util/secure_vault.h
T
DanSandClaude Opus 4.8 6301cd5984 fix(security): synchronize SecureVault::scope_ between the main thread and worker security ops
Concurrency audit: SecureVault::scope_ (std::string) is written by setWalletScope() on the main thread
(wallet switch) while getVaultPath() — reached from retrieve()/hasVault()/changePin()/store() — runs on
RPC worker threads during a send-time or startup unlock. A concurrent wallet switch and a worker vault op
is a data race on the string (and could resolve the wrong vault file).

Guard scope_ with a small mutex: setWalletScope() assigns under it; getVaultPath() copies it out under the
lock and uses the copy. The lock is held only for the string copy — never across Argon2id or file I/O —
and getVaultPath() never calls setWalletScope(), so no deadlock. Argon2id params, key derivation, AEAD,
the vault file format and retrieve/store/changePin are byte-identical. SecureVault is held via unique_ptr
and used by pointer, so the added (non-movable) mutex is fine.

Synchronization only. Threading fix — clean build + ctest; the race is runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:34:59 -05:00

113 lines
3.6 KiB
C++

// ObsidianDragon Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <mutex>
namespace dragonx {
namespace util {
/**
* @brief PIN-encrypted passphrase vault
*
* Stores the wallet passphrase encrypted with a PIN-derived key using
* libsodium's Argon2id (key derivation) + XSalsa20-Poly1305 (AEAD).
*
* File format (vault.dat):
* [1 byte] version (0x01)
* [16 bytes] Argon2id salt
* [24 bytes] XSalsa20 nonce
* [N bytes] ciphertext (passphrase + 16-byte Poly1305 tag)
*
* The PIN is 4-8 digits. Argon2id uses moderate ops/mem limits
* to resist brute-force on the small PIN space while keeping
* unlock time under ~1 second.
*/
class SecureVault {
public:
// `walletFile` scopes the vault to a specific wallet.dat so one wallet's PIN passphrase is never
// offered for another. The default wallet ("wallet.dat" / empty) keeps the legacy global vault.dat
// path for backward compatibility; other wallets get their own vault-<scope>.dat.
explicit SecureVault(const std::string& walletFile = "");
~SecureVault();
// Re-scope to a different wallet (called when the active wallet changes).
void setWalletScope(const std::string& walletFile);
/**
* @brief Check if a vault file exists on disk
*/
bool hasVault() const;
/**
* @brief Cheap structural check of the vault file (no PIN, no Argon2): at least the minimum
* size and the known version byte first. A zero-filled file (left by a removeVault() whose
* unlink failed) or a truncated one is not well formed, and no PIN can open it.
*/
bool isWellFormed() const;
/**
* @brief Encrypt passphrase with PIN and save to vault.dat
* @param pin The user's PIN (4-8 digits)
* @param passphrase The wallet passphrase to protect
* @return true on success
*/
bool store(const std::string& pin, const std::string& passphrase);
/**
* @brief Decrypt passphrase from vault using PIN
* @param pin The user's PIN
* @param[out] passphrase The recovered passphrase
* @return true if PIN was correct and decryption succeeded
*/
bool retrieve(const std::string& pin, std::string& passphrase) const;
/**
* @brief Change the PIN (re-encrypt with new PIN)
* @param oldPin Current PIN
* @param newPin New PIN
* @return true if old PIN was valid and re-encryption succeeded
*/
bool changePin(const std::string& oldPin, const std::string& newPin);
/**
* @brief Remove the vault file
* @return true if deleted or didn't exist
*/
bool removeVault();
/**
* @brief Validate PIN format (4-8 digits)
*/
static bool isValidPin(const std::string& pin);
/**
* @brief Securely zero memory
*/
static void secureZero(void* ptr, size_t len);
/**
* @brief Get this vault's file path (scoped to its wallet)
*/
std::string getVaultPath() const;
private:
// Derive a 32-byte key from PIN + salt using Argon2id
bool deriveKey(const std::string& pin,
const uint8_t* salt, size_t saltLen,
uint8_t* key, size_t keyLen) const;
// Guards scope_: setWalletScope() runs on the main thread (wallet switch) while retrieve()/hasVault()/
// changePin() run on RPC worker threads and read it via getVaultPath().
mutable std::mutex scopeMutex_;
std::string scope_; // "" = default/legacy wallet (vault.dat); else a sanitized wallet-file tag
};
} // namespace util
} // namespace dragonx