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>
113 lines
3.6 KiB
C++
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
|