// ObsidianDragon Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include 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-.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