diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 42e2f1d4183..875360c5c10 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1003,6 +1003,24 @@ uint64_t num_priv_multisig_keys_post_setup(uint64_t threshold, uint64_t total) return n_multisig_keys; } +/** + * @brief Derives the chacha key to encrypt wallet cache files given the chacha key to encrypt the wallet keys files + * + * @param keys_data_key the chacha key that encrypts wallet keys files + * @return crypto::chacha_key the chacha key that encrypts the wallet cache files + */ +crypto::chacha_key derive_cache_key(const crypto::chacha_key& keys_data_key) +{ + static_assert(HASH_SIZE == sizeof(crypto::chacha_key), "Mismatched sizes of hash and chacha key"); + + crypto::chacha_key cache_key; + epee::mlocked> cache_key_data; + memcpy(cache_key_data.data(), &keys_data_key, HASH_SIZE); + cache_key_data[HASH_SIZE] = config::HASH_KEY_WALLET_CACHE; + cn_fast_hash(cache_key_data.data(), HASH_SIZE+1, (crypto::hash&) cache_key); + + return cache_key; +} //----------------------------------------------------------------- } //namespace @@ -4406,6 +4424,10 @@ boost::optional wallet2::get_keys_file_data(const epee: crypto::chacha_key key; crypto::generate_chacha_key(password.data(), password.size(), key, m_kdf_rounds); + // We use m_cache_key as a deterministic test to see if given key corresponds to original password + const crypto::chacha_key cache_key = derive_cache_key(key); + THROW_WALLET_EXCEPTION_IF(cache_key != m_cache_key, error::invalid_password); + if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only) { account.encrypt_viewkey(key); @@ -4630,11 +4652,8 @@ void wallet2::setup_keys(const epee::wipeable_string &password) m_account.decrypt_viewkey(key); } - static_assert(HASH_SIZE == sizeof(crypto::chacha_key), "Mismatched sizes of hash and chacha key"); - epee::mlocked> cache_key_data; - memcpy(cache_key_data.data(), &key, HASH_SIZE); - cache_key_data[HASH_SIZE] = config::HASH_KEY_WALLET_CACHE; - cn_fast_hash(cache_key_data.data(), HASH_SIZE+1, (crypto::hash&)m_cache_key); + m_cache_key = derive_cache_key(key); + get_ringdb_key(); } //---------------------------------------------------------------------------------------------------- @@ -4643,9 +4662,8 @@ void wallet2::change_password(const std::string &filename, const epee::wipeable_ if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only) decrypt_keys(original_password); setup_keys(new_password); - rewrite(filename, new_password); if (!filename.empty()) - store(); + store_to(filename, new_password, true); // force rewrite keys file to possible new location } //---------------------------------------------------------------------------------------------------- /*! @@ -5151,6 +5169,10 @@ void wallet2::encrypt_keys(const crypto::chacha_key &key) void wallet2::decrypt_keys(const crypto::chacha_key &key) { + // We use m_cache_key as a deterministic test to see if given key corresponds to original password + const crypto::chacha_key cache_key = derive_cache_key(key); + THROW_WALLET_EXCEPTION_IF(cache_key != m_cache_key, error::invalid_password); + m_account.encrypt_viewkey(key); m_account.decrypt_keys(key); } @@ -6311,22 +6333,32 @@ void wallet2::store() store_to("", epee::wipeable_string()); } //---------------------------------------------------------------------------------------------------- -void wallet2::store_to(const std::string &path, const epee::wipeable_string &password) +void wallet2::store_to(const std::string &path, const epee::wipeable_string &password, bool force_rewrite_keys) { trim_hashchain(); + const bool had_old_wallet_files = !m_wallet_file.empty(); + THROW_WALLET_EXCEPTION_IF(!had_old_wallet_files && path.empty(), error::wallet_internal_error, + "Cannot resave wallet to current file since wallet was not loaded from file to begin with"); + // if file is the same, we do: - // 1. save wallet to the *.new file - // 2. remove old wallet file - // 3. rename *.new to wallet_name + // 1. overwrite the keys file iff force_rewrite_keys is specified + // 2. save cache to the *.new file + // 3. rename *.new to wallet_name, replacing old cache file + // else we do: + // 1. prepare new file names with "path" variable + // 2. store new keys files + // 3. remove old keys file + // 4. store new cache file + // 5. remove old cache file // handle if we want just store wallet state to current files (ex store() replacement); - bool same_file = true; - if (!path.empty()) + bool same_file = had_old_wallet_files && path.empty(); + if (had_old_wallet_files && !path.empty()) { - std::string canonical_path = boost::filesystem::canonical(m_wallet_file).string(); - size_t pos = canonical_path.find(path); - same_file = pos != std::string::npos; + const std::string canonical_old_path = boost::filesystem::canonical(m_wallet_file).string(); + const std::string canonical_new_path = boost::filesystem::weakly_canonical(path).string(); + same_file = canonical_old_path == canonical_new_path; } @@ -6347,7 +6379,7 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas } // get wallet cache data - boost::optional cache_file_data = get_cache_file_data(password); + boost::optional cache_file_data = get_cache_file_data(); THROW_WALLET_EXCEPTION_IF(cache_file_data == boost::none, error::wallet_internal_error, "failed to generate wallet cache data"); const std::string new_file = same_file ? m_wallet_file + ".new" : path; @@ -6356,12 +6388,20 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas const std::string old_address_file = m_wallet_file + ".address.txt"; const std::string old_mms_file = m_mms_file; - // save keys to the new file - // if we here, main wallet file is saved and we only need to save keys and address files - if (!same_file) { + if (!same_file) + { prepare_file_names(path); + } + + if (!same_file || force_rewrite_keys) + { bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + } + + if (!same_file && had_old_wallet_files) + { + bool r = false; if (boost::filesystem::exists(old_address_file)) { // save address to the new file @@ -6374,11 +6414,6 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas LOG_ERROR("error removing file: " << old_address_file); } } - // remove old wallet file - r = boost::filesystem::remove(old_file); - if (!r) { - LOG_ERROR("error removing file: " << old_file); - } // remove old keys file r = boost::filesystem::remove(old_keys_file); if (!r) { @@ -6392,8 +6427,9 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas LOG_ERROR("error removing file: " << old_mms_file); } } - } else { - // save to new file + } + + // Save cache to new file. If storing to the same file, the temp path has the ".new" extension #ifdef WIN32 // On Windows avoid using std::ofstream which does not work with UTF-8 filenames // The price to pay is temporary higher memory consumption for string stream + binary archive @@ -6413,10 +6449,20 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas THROW_WALLET_EXCEPTION_IF(!success || !ostr.good(), error::file_save_error, new_file); #endif + if (same_file) + { // here we have "*.new" file, we need to rename it to be without ".new" std::error_code e = tools::replace_file(new_file, m_wallet_file); THROW_WALLET_EXCEPTION_IF(e, error::file_save_error, m_wallet_file, e); } + else if (!same_file && had_old_wallet_files) + { + // remove old wallet file + bool r = boost::filesystem::remove(old_file); + if (!r) { + LOG_ERROR("error removing file: " << old_file); + } + } if (m_message_store.get_active()) { @@ -6426,7 +6472,7 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas } } //---------------------------------------------------------------------------------------------------- -boost::optional wallet2::get_cache_file_data(const epee::wipeable_string &passwords) +boost::optional wallet2::get_cache_file_data() { trim_hashchain(); try diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index baeffe096c6..3790f7121aa 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -940,22 +940,32 @@ namespace tools /*! * \brief store_to Stores wallet to another file(s), deleting old ones * \param path Path to the wallet file (keys and address filenames will be generated based on this filename) - * \param password Password to protect new wallet (TODO: probably better save the password in the wallet object?) + * \param password Password that currently locks the wallet + * \param force_rewrite_keys if true, always rewrite keys file + * + * Leave both "path" and "password" blank to restore the cache file to the current position in the disk + * (which is the same as calling `store()`). If you want to store the wallet with a new password, + * use the method `change_password()`. + * + * Normally the keys file is not overwritten when storing, except when force_rewrite_keys is true + * or when `path` is a new wallet file. + * + * \throw error::invalid_password If storing keys file and old password is incorrect */ - void store_to(const std::string &path, const epee::wipeable_string &password); + void store_to(const std::string &path, const epee::wipeable_string &password, bool force_rewrite_keys = false); /*! * \brief get_keys_file_data Get wallet keys data which can be stored to a wallet file. - * \param password Password of the encrypted wallet buffer (TODO: probably better save the password in the wallet object?) + * \param password Password that currently locks the wallet * \param watch_only true to include only view key, false to include both spend and view keys * \return Encrypted wallet keys data which can be stored to a wallet file + * \throw error::invalid_password if password does not match current wallet */ boost::optional get_keys_file_data(const epee::wipeable_string& password, bool watch_only); /*! * \brief get_cache_file_data Get wallet cache data which can be stored to a wallet file. - * \param password Password to protect the wallet cache data (TODO: probably better save the password in the wallet object?) - * \return Encrypted wallet cache data which can be stored to a wallet file + * \return Encrypted wallet cache data which can be stored to a wallet file (using current password) */ - boost::optional get_cache_file_data(const epee::wipeable_string& password); + boost::optional get_cache_file_data(); std::string path() const; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2cabb1ba5bc..e074ceed638 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,14 +72,8 @@ else () include_directories(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/gtest/include") endif (GTest_FOUND) -file(COPY - data/wallet_9svHk1.keys - data/wallet_9svHk1 - data/outputs - data/unsigned_monero_tx - data/signed_monero_tx - data/sha256sum - DESTINATION data) +message(STATUS "Copying test data directory...") +file(COPY data DESTINATION .) # Copy data directory from source root to build root if (CMAKE_BUILD_TYPE STREQUAL "fuzz" OR OSSFUZZ) add_subdirectory(fuzz) diff --git a/tests/data/wallet_00fd416a b/tests/data/wallet_00fd416a new file mode 100644 index 00000000000..a1b7898e63c Binary files /dev/null and b/tests/data/wallet_00fd416a differ diff --git a/tests/data/wallet_00fd416a.keys b/tests/data/wallet_00fd416a.keys new file mode 100644 index 00000000000..6908cce1b82 Binary files /dev/null and b/tests/data/wallet_00fd416a.keys differ diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 147b38dd45a..fec36803eba 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -97,6 +97,7 @@ set(unit_tests_sources output_selection.cpp vercmp.cpp ringdb.cpp + wallet_storage.cpp wipeable_string.cpp is_hdd.cpp aligned.cpp diff --git a/tests/unit_tests/wallet_storage.cpp b/tests/unit_tests/wallet_storage.cpp new file mode 100644 index 00000000000..dacaff9602e --- /dev/null +++ b/tests/unit_tests/wallet_storage.cpp @@ -0,0 +1,266 @@ +// Copyright (c) 2023, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "unit_tests_utils.h" +#include "gtest/gtest.h" + +#include "file_io_utils.h" +#include "wallet/wallet2.h" + +using namespace boost::filesystem; +using namespace epee::file_io_utils; + +static constexpr const char WALLET_00fd416a_PRIMARY_ADDRESS[] = + "45p2SngJAPSJbqSiUvYfS3BfhEdxZmv8pDt25oW1LzxrZv9Uq6ARagiFViMGUE3gJk5VPWingCXVf1p2tyAy6SUeSHPhbve"; + +TEST(wallet_storage, store_to_file2file) +{ + const path source_wallet_file = unit_test::data_dir / "wallet_00fd416a"; + const path interm_wallet_file = unit_test::data_dir / "wallet_00fd416a_copy_file2file"; + const path target_wallet_file = unit_test::data_dir / "wallet_00fd416a_new_file2file"; + + ASSERT_TRUE(is_file_exist(source_wallet_file.string())); + ASSERT_TRUE(is_file_exist(source_wallet_file.string() + ".keys")); + + copy_file(source_wallet_file, interm_wallet_file, copy_option::overwrite_if_exists); + copy_file(source_wallet_file.string() + ".keys", interm_wallet_file.string() + ".keys", copy_option::overwrite_if_exists); + + ASSERT_TRUE(is_file_exist(interm_wallet_file.string())); + ASSERT_TRUE(is_file_exist(interm_wallet_file.string() + ".keys")); + + if (is_file_exist(target_wallet_file.string())) + remove(target_wallet_file); + if (is_file_exist(target_wallet_file.string() + ".keys")) + remove(target_wallet_file.string() + ".keys"); + ASSERT_FALSE(is_file_exist(target_wallet_file.string())); + ASSERT_FALSE(is_file_exist(target_wallet_file.string() + ".keys")); + + epee::wipeable_string password("beepbeep"); + + const auto files_are_expected = [&]() + { + EXPECT_FALSE(is_file_exist(interm_wallet_file.string())); + EXPECT_FALSE(is_file_exist(interm_wallet_file.string() + ".keys")); + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); + }; + + { + tools::wallet2 w; + w.load(interm_wallet_file.string(), password); + const std::string primary_address = w.get_address_as_str(); + EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); + w.store_to(target_wallet_file.string(), password); + files_are_expected(); + } + + files_are_expected(); + + { + tools::wallet2 w; + w.load(target_wallet_file.string(), password); + const std::string primary_address = w.get_address_as_str(); + EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); + w.store_to("", ""); + files_are_expected(); + } + + files_are_expected(); +} + +TEST(wallet_storage, store_to_mem2file) +{ + const path target_wallet_file = unit_test::data_dir / "wallet_mem2file"; + + if (is_file_exist(target_wallet_file.string())) + remove(target_wallet_file); + if (is_file_exist(target_wallet_file.string() + ".keys")) + remove(target_wallet_file.string() + ".keys"); + ASSERT_FALSE(is_file_exist(target_wallet_file.string())); + ASSERT_FALSE(is_file_exist(target_wallet_file.string() + ".keys")); + + epee::wipeable_string password("beepbeep2"); + + { + tools::wallet2 w; + w.generate("", password); + w.store_to(target_wallet_file.string(), password); + + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); + } + + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); + + { + tools::wallet2 w; + w.load(target_wallet_file.string(), password); + + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); + } + + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); +} + +TEST(wallet_storage, change_password_same_file) +{ + const path source_wallet_file = unit_test::data_dir / "wallet_00fd416a"; + const path interm_wallet_file = unit_test::data_dir / "wallet_00fd416a_copy_change_password_same"; + + ASSERT_TRUE(is_file_exist(source_wallet_file.string())); + ASSERT_TRUE(is_file_exist(source_wallet_file.string() + ".keys")); + + copy_file(source_wallet_file, interm_wallet_file, copy_option::overwrite_if_exists); + copy_file(source_wallet_file.string() + ".keys", interm_wallet_file.string() + ".keys", copy_option::overwrite_if_exists); + + ASSERT_TRUE(is_file_exist(interm_wallet_file.string())); + ASSERT_TRUE(is_file_exist(interm_wallet_file.string() + ".keys")); + + epee::wipeable_string old_password("beepbeep"); + epee::wipeable_string new_password("meepmeep"); + + { + tools::wallet2 w; + w.load(interm_wallet_file.string(), old_password); + const std::string primary_address = w.get_address_as_str(); + EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); + w.change_password(w.get_wallet_file(), old_password, new_password); + } + + { + tools::wallet2 w; + w.load(interm_wallet_file.string(), new_password); + const std::string primary_address = w.get_address_as_str(); + EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); + } + + { + tools::wallet2 w; + EXPECT_THROW(w.load(interm_wallet_file.string(), old_password), tools::error::invalid_password); + } +} + +TEST(wallet_storage, change_password_different_file) +{ + const path source_wallet_file = unit_test::data_dir / "wallet_00fd416a"; + const path interm_wallet_file = unit_test::data_dir / "wallet_00fd416a_copy_change_password_diff"; + const path target_wallet_file = unit_test::data_dir / "wallet_00fd416a_new_change_password_diff"; + + ASSERT_TRUE(is_file_exist(source_wallet_file.string())); + ASSERT_TRUE(is_file_exist(source_wallet_file.string() + ".keys")); + + copy_file(source_wallet_file, interm_wallet_file, copy_option::overwrite_if_exists); + copy_file(source_wallet_file.string() + ".keys", interm_wallet_file.string() + ".keys", copy_option::overwrite_if_exists); + + ASSERT_TRUE(is_file_exist(interm_wallet_file.string())); + ASSERT_TRUE(is_file_exist(interm_wallet_file.string() + ".keys")); + + if (is_file_exist(target_wallet_file.string())) + remove(target_wallet_file); + if (is_file_exist(target_wallet_file.string() + ".keys")) + remove(target_wallet_file.string() + ".keys"); + ASSERT_FALSE(is_file_exist(target_wallet_file.string())); + ASSERT_FALSE(is_file_exist(target_wallet_file.string() + ".keys")); + + epee::wipeable_string old_password("beepbeep"); + epee::wipeable_string new_password("meepmeep"); + + { + tools::wallet2 w; + w.load(interm_wallet_file.string(), old_password); + const std::string primary_address = w.get_address_as_str(); + EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); + w.change_password(target_wallet_file.string(), old_password, new_password); + } + + EXPECT_FALSE(is_file_exist(interm_wallet_file.string())); + EXPECT_FALSE(is_file_exist(interm_wallet_file.string() + ".keys")); + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); + + { + tools::wallet2 w; + w.load(target_wallet_file.string(), new_password); + const std::string primary_address = w.get_address_as_str(); + EXPECT_EQ(WALLET_00fd416a_PRIMARY_ADDRESS, primary_address); + } +} + +TEST(wallet_storage, change_password_in_memory) +{ + const epee::wipeable_string password1("monero"); + const epee::wipeable_string password2("means money"); + const epee::wipeable_string password_wrong("is traceable"); + + tools::wallet2 w; + w.generate("", password1); + const std::string primary_address_1 = w.get_address_as_str(); + w.change_password("", password1, password2); + const std::string primary_address_2 = w.get_address_as_str(); + EXPECT_EQ(primary_address_1, primary_address_2); + + EXPECT_THROW(w.change_password("", password_wrong, password1), tools::error::invalid_password); +} + +TEST(wallet_storage, change_password_mem2file) +{ + const path target_wallet_file = unit_test::data_dir / "wallet_change_password_mem2file"; + + if (is_file_exist(target_wallet_file.string())) + remove(target_wallet_file); + if (is_file_exist(target_wallet_file.string() + ".keys")) + remove(target_wallet_file.string() + ".keys"); + ASSERT_FALSE(is_file_exist(target_wallet_file.string())); + ASSERT_FALSE(is_file_exist(target_wallet_file.string() + ".keys")); + + const epee::wipeable_string password1("https://safecurves.cr.yp.to/rigid.html"); + const epee::wipeable_string password2( + "https://csrc.nist.gov/csrc/media/projects/crypto-standards-development-process/documents/dualec_in_x982_and_sp800-90.pdf"); + + std::string primary_address_1, primary_address_2; + { + tools::wallet2 w; + w.generate("", password1); + primary_address_1 = w.get_address_as_str(); + w.change_password(target_wallet_file.string(), password1, password2); + } + + EXPECT_TRUE(is_file_exist(target_wallet_file.string())); + EXPECT_TRUE(is_file_exist(target_wallet_file.string() + ".keys")); + + { + tools::wallet2 w; + w.load(target_wallet_file.string(), password2); + primary_address_2 = w.get_address_as_str(); + } + + EXPECT_EQ(primary_address_1, primary_address_2); +}