diff --git a/CMakeLists.txt b/CMakeLists.txt index ed321a972d..4c73c29b06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1097,6 +1097,7 @@ endif() include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) if(MINGW) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wa,-mbig-obj") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wa,-mbig-obj") set(EXTRA_LIBRARIES mswsock;ws2_32;iphlpapi;crypt32;bcrypt) if(DEPENDS) set(ICU_LIBRARIES icuio icui18n icuuc icudata icutu iconv) diff --git a/src/serialization/polymorphic_portable_binary_iarchive.h b/src/serialization/polymorphic_portable_binary_iarchive.h new file mode 100644 index 0000000000..12439f6104 --- /dev/null +++ b/src/serialization/polymorphic_portable_binary_iarchive.h @@ -0,0 +1,56 @@ +// 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. +// + +#pragma once + +#include +#include +#include + +namespace boost +{ +namespace archive +{ +class polymorphic_portable_binary_iarchive : + public detail::polymorphic_iarchive_route +{ +public: + polymorphic_portable_binary_iarchive(std::istream & is, unsigned int flags = 0) : + detail::polymorphic_iarchive_route(is, flags) + {} + ~polymorphic_portable_binary_iarchive() {} +}; + +} // namespace archive +} // namespace boost + +// required by export +BOOST_SERIALIZATION_REGISTER_ARCHIVE( + boost::archive::polymorphic_portable_binary_iarchive +) diff --git a/src/serialization/polymorphic_portable_binary_oarchive.h b/src/serialization/polymorphic_portable_binary_oarchive.h new file mode 100644 index 0000000000..aa6b03738f --- /dev/null +++ b/src/serialization/polymorphic_portable_binary_oarchive.h @@ -0,0 +1,56 @@ +// 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. +// + +#pragma once + +#include +#include +#include + +namespace boost +{ +namespace archive +{ +class polymorphic_portable_binary_oarchive : + public detail::polymorphic_oarchive_route +{ +public: + polymorphic_portable_binary_oarchive(std::ostream & os, unsigned int flags = 0) : + detail::polymorphic_oarchive_route(os, flags) + {} + ~polymorphic_portable_binary_oarchive() {} +}; + +} // namespace archive +} // namespace boost + +// required by export +BOOST_SERIALIZATION_REGISTER_ARCHIVE( + boost::archive::polymorphic_portable_binary_oarchive +) diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 48c7f1c5b2..69f4f4d830 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -104,3 +104,4 @@ if(NOT IOS) endif() add_subdirectory(api) +add_subdirectory(wallet2_basic) diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 944fcb86ad..836a593479 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -93,6 +93,8 @@ namespace tools class wallet2; class Notify; + extern void check_wallet_9svHk1_cache_contents(const wallet2&); + class gamma_picker { public: @@ -225,6 +227,7 @@ namespace tools { friend class ::Serialization_portability_wallet_Test; friend class ::wallet_accessor_test; + friend void check_wallet_9svHk1_cache_contents(const wallet2&); friend class wallet_keys_unlocker; friend class wallet_device_callback; public: diff --git a/src/wallet/wallet2_basic/CMakeLists.txt b/src/wallet/wallet2_basic/CMakeLists.txt new file mode 100644 index 0000000000..eae5c44559 --- /dev/null +++ b/src/wallet/wallet2_basic/CMakeLists.txt @@ -0,0 +1,74 @@ +# 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. + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +set(wallet2_basic_sources + wallet2_boost_serialization.cpp + wallet2_storage.cpp +) + +set(wallet2_basic_headers + wallet2_constants.h + wallet2_storage.h + wallet2_types.h +) + +set(wallet2_basic_private_headers +) + +monero_private_headers(wallet2_basic + ${wallet2_basic_private_headers}) +monero_add_library(wallet2_basic + ${wallet2_basic_sources} + ${wallet2_basic_headers} + ${wallet2_basic_private_headers}) +target_link_libraries(wallet2_basic + PUBLIC + common + cryptonote_core + device_trezor + ${Boost_LOCALE_LIBRARY} + ${ICU_LIBRARIES} + ${Boost_SERIALIZATION_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_SYSTEM_LIBRARY} + ${OPENSSL_LIBRARIES} + PRIVATE + ${EXTRA_LIBRARIES}) + +set_property(TARGET wallet2_basic PROPERTY EXCLUDE_FROM_ALL TRUE) + +if(IOS) + set(lib_folder lib-${ARCH}) +else() + set(lib_folder lib) +endif() + +install(FILES ${wallet2_basic_headers} + DESTINATION include/wallet/wallet2_basic) diff --git a/src/wallet/wallet2_basic/wallet2_boost_serialization.cpp b/src/wallet/wallet2_basic/wallet2_boost_serialization.cpp new file mode 100644 index 0000000000..008f84776b --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_boost_serialization.cpp @@ -0,0 +1,39 @@ +// 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 "wallet2_boost_serialization.h" + +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::hashchain) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::transfer_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::multisig_info) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::unconfirmed_transfer_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::confirmed_transfer_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::payment_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::pool_payment_details) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::address_book_row) +BOOST_CLASS_EXPORT_IMPLEMENT(::wallet2_basic::cache) diff --git a/src/wallet/wallet2_basic/wallet2_boost_serialization.h b/src/wallet/wallet2_basic/wallet2_boost_serialization.h new file mode 100644 index 0000000000..f0e77d7055 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_boost_serialization.h @@ -0,0 +1,559 @@ +// 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. + +#pragma once + +#include +#include +#include +#include + +#include "cryptonote_basic/cryptonote_boost_serialization.h" +#include "cryptonote_basic/account_boost_serialization.h" +#include "serialization/polymorphic_portable_binary_iarchive.h" +#include "serialization/polymorphic_portable_binary_oarchive.h" +#include "wallet2_storage.h" +#include "wallet2_types.h" + +namespace wallet2_basic +{ +struct HashchainAccessor +{ + hashchain& hc; + HashchainAccessor(hashchain& hc): hc(hc) {} + inline std::size_t& m_offset() { return hc.m_offset; } + inline crypto::hash& m_genesis() { return hc.m_genesis; } + inline std::deque& m_blockchain() { return hc.m_blockchain; } +}; +} // namespace wallet2_basic + +namespace boost +{ +namespace serialization +{ +template +void serialize(Archive &a, wallet2_basic::multisig_info::LR &x, const version_type ver) +{ + a & x.m_L; + a & x.m_R; +} + +template +void serialize(Archive &a, wallet2_basic::multisig_info &x, const version_type ver) +{ + a & x.m_signer; + a & x.m_LR; + a & x.m_partial_key_images; +} + +template +std::enable_if_t +initialize_transfer_details(Archive &a, wallet2_basic::transfer_details &x, const version_type ver) +{} + +template +std::enable_if_t +initialize_transfer_details(Archive &a, wallet2_basic::transfer_details &x, const version_type ver) +{ + if (ver < 1) + { + x.m_mask = rct::identity(); + x.m_amount = x.m_tx.vout[x.m_internal_output_index].amount; + } + if (ver < 2) + { + x.m_spent_height = 0; + } + if (ver < 4) + { + x.m_rct = x.m_tx.vout[x.m_internal_output_index].amount == 0; + } + if (ver < 6) + { + x.m_key_image_known = true; + } + if (ver < 7) + { + x.m_pk_index = 0; + } + if (ver < 8) + { + x.m_subaddr_index = {}; + } + if (ver < 9) + { + x.m_key_image_partial = false; + x.m_multisig_k.clear(); + x.m_multisig_info.clear(); + } + if (ver < 10) + { + x.m_key_image_request = false; + } + if (ver < 12) + { + x.m_frozen = false; + } +} + +template +void serialize(Archive &a, wallet2_basic::hashchain &x, const version_type ver) +{ + wallet2_basic::HashchainAccessor xaccess(x); + a & xaccess.m_offset(); + a & xaccess.m_genesis(); + a & xaccess.m_blockchain(); +} + +template +void serialize(Archive &a, wallet2_basic::transfer_details &x, const version_type ver) +{ + a & x.m_block_height; + a & x.m_global_output_index; + a & x.m_internal_output_index; + if (ver < 3) + { + cryptonote::transaction tx; + a & tx; + x.m_tx = (const cryptonote::transaction_prefix&)tx; + x.m_txid = cryptonote::get_transaction_hash(tx); + } + else + { + a & x.m_tx; + } + a & x.m_spent; + a & x.m_key_image; + if (ver < 1) + { + // ensure mask and amount are set + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_mask; + a & x.m_amount; + if (ver < 2) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_spent_height; + if (ver < 3) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_txid; + if (ver < 4) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_rct; + if (ver < 5) + { + initialize_transfer_details(a, x, ver); + return; + } + if (ver < 6) + { + // v5 did not properly initialize + uint8_t u = 0; + a & u; + x.m_key_image_known = true; + return; + } + a & x.m_key_image_known; + if (ver < 7) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_pk_index; + if (ver < 8) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_subaddr_index; + if (ver < 9) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_multisig_info; + a & x.m_multisig_k; + a & x.m_key_image_partial; + if (ver < 10) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_key_image_request; + if (ver < 11) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_uses; + if (ver < 12) + { + initialize_transfer_details(a, x, ver); + return; + } + a & x.m_frozen; +} + +template +void serialize(Archive &a, wallet2_basic::unconfirmed_transfer_details &x, const version_type ver) +{ + a & x.m_change; + a & x.m_sent_time; + if (ver < 5) + { + cryptonote::transaction tx; + a & tx; + x.m_tx = (const cryptonote::transaction_prefix&)tx; + } + else + { + a & x.m_tx; + } + if (ver < 1) + return; + a & x.m_dests; + a & x.m_payment_id; + if (ver < 2) + return; + a & x.m_state; + if (ver < 3) + return; + a & x.m_timestamp; + if (ver < 4) + return; + a & x.m_amount_in; + a & x.m_amount_out; + if (ver < 6) + { + // v<6 may not have change accumulated in m_amount_out, which is a pain, + // as it's readily understood to be sum of outputs. + // We convert it to include change from v6 + if (!typename Archive::is_saving() && x.m_change != (uint64_t)1) + x.m_amount_out += x.m_change; + } + if (ver < 7) + { + x.m_subaddr_account = 0; + return; + } + a & x.m_subaddr_account; + a & x.m_subaddr_indices; + if (ver < 8) + return; + a & x.m_rings; +} + +template +void serialize(Archive &a, wallet2_basic::confirmed_transfer_details &x, const version_type ver) +{ + a & x.m_amount_in; + a & x.m_amount_out; + a & x.m_change; + a & x.m_block_height; + if (ver < 1) + return; + a & x.m_dests; + a & x.m_payment_id; + if (ver < 2) + return; + a & x.m_timestamp; + if (ver < 3) + { + // v<3 may not have change accumulated in m_amount_out, which is a pain, + // as it's readily understood to be sum of outputs. Whether it got added + // or not depends on whether it came from a unconfirmed_transfer_details + // (not included) or not (included). We can't reliably tell here, so we + // check whether either yields a "negative" fee, or use the other if so. + // We convert it to include change from v3 + if (!typename Archive::is_saving() && x.m_change != (uint64_t)1) + { + if (x.m_amount_in > (x.m_amount_out + x.m_change)) + x.m_amount_out += x.m_change; + } + } + if (ver < 4) + { + if (!typename Archive::is_saving()) + x.m_unlock_time = 0; + return; + } + a & x.m_unlock_time; + if (ver < 5) + { + x.m_subaddr_account = 0; + return; + } + a & x.m_subaddr_account; + a & x.m_subaddr_indices; + if (ver < 6) + return; + a & x.m_rings; +} + +template +void serialize(Archive& a, wallet2_basic::payment_details& x, const version_type ver) +{ + a & x.m_tx_hash; + a & x.m_amount; + a & x.m_block_height; + a & x.m_unlock_time; + if (ver < 1) + return; + a & x.m_timestamp; + if (ver < 2) + { + x.m_coinbase = false; + x.m_subaddr_index = {}; + return; + } + a & x.m_subaddr_index; + if (ver < 3) + { + x.m_coinbase = false; + x.m_fee = 0; + return; + } + a & x.m_fee; + if (ver < 4) + { + x.m_coinbase = false; + return; + } + a & x.m_coinbase; + if (ver < 5) + return; + a & x.m_amounts; +} + +template +void serialize(Archive& a, wallet2_basic::pool_payment_details& x, const version_type ver) +{ + a & x.m_pd; + a & x.m_double_spend_seen; +} + +template +void serialize(Archive& a, wallet2_basic::address_book_row& x, const version_type ver) +{ + a & x.m_address; + if (ver < 18) + { + crypto::hash payment_id; + a & payment_id; + x.m_has_payment_id = !(payment_id == crypto::null_hash); + if (x.m_has_payment_id) + { + bool is_long = false; + for (int i = 8; i < 32; ++i) + is_long |= payment_id.data[i]; + if (is_long) + { + MWARNING("Long payment ID ignored on address book load"); + x.m_payment_id = crypto::null_hash8; + x.m_has_payment_id = false; + } + else + memcpy(x.m_payment_id.data, payment_id.data, 8); + } + } + a & x.m_description; + if (ver < 17) + { + x.m_is_subaddress = false; + return; + } + a & x.m_is_subaddress; + if (ver < 18) + return; + a & x.m_has_payment_id; + if (x.m_has_payment_id) + a & x.m_payment_id; +} + +template +void serialize(Archive& a, wallet2_basic::cache& x, const version_type ver) +{ + using namespace wallet2_basic; + + uint64_t dummy_refresh_height = 0; // moved to keys file + if(ver < 5) + return; + if (ver < 19) + { + std::vector blockchain; + a & blockchain; + x.m_blockchain.clear(); + for (const auto &b: blockchain) + { + x.m_blockchain.push_back(b); + } + } + else + { + a & x.m_blockchain; + } + a & x.m_transfers; + a & x.m_account_public_address; + a & x.m_key_images.parent(); + if(ver < 6) + return; + a & x.m_unconfirmed_txs.parent(); + if(ver < 7) + return; + a & x.m_payments.parent(); + if(ver < 8) + return; + a & x.m_tx_keys.parent(); + if(ver < 9) + return; + a & x.m_confirmed_txs.parent(); + if(ver < 11) + return; + a & dummy_refresh_height; + if(ver < 12) + return; + a & x.m_tx_notes.parent(); + if(ver < 13) + return; + if (ver < 17) + { + // we're loading an old version, where m_unconfirmed_payments was a std::map + std::unordered_map m; + a & m; + x.m_unconfirmed_payments.clear(); + for (const auto& i : m) + x.m_unconfirmed_payments.insert({i.first, pool_payment_details{i.second, false}}); + } + if(ver < 14) + return; + if(ver < 15) + { + // we're loading an older wallet without a pubkey map, rebuild it + x.m_pub_keys.clear(); + for (size_t i = 0; i < x.m_transfers.size(); ++i) + { + const transfer_details &td = x.m_transfers[i]; + x.m_pub_keys.emplace(td.get_public_key(), i); + } + return; + } + a & x.m_pub_keys.parent(); + if(ver < 16) + return; + a & x.m_address_book; + if(ver < 17) + return; + if (ver < 22) + { + // we're loading an old version, where m_unconfirmed_payments payload was payment_details + std::unordered_multimap m; + a & m; + x.m_unconfirmed_payments.clear(); + for (const auto &i: m) + x.m_unconfirmed_payments.insert({i.first, pool_payment_details{i.second, false}}); + } + if(ver < 18) + return; + a & x.m_scanned_pool_txs[0]; + a & x.m_scanned_pool_txs[1]; + if (ver < 20) + return; + a & x.m_subaddresses.parent(); + std::unordered_map dummy_subaddresses_inv; + a & dummy_subaddresses_inv; + a & x.m_subaddress_labels; + a & x.m_additional_tx_keys.parent(); + if(ver < 21) + return; + a & x.m_attributes.parent(); + if(ver < 22) + return; + a & x.m_unconfirmed_payments.parent(); + if(ver < 23) + return; + a & (std::pair, std::vector>&) x.m_account_tags; + if(ver < 24) + return; + a & x.m_ring_history_saved; + if(ver < 25) + return; + a & x.m_last_block_reward; + if(ver < 26) + return; + a & x.m_tx_device.parent(); + if(ver < 27) + return; + a & x.m_device_last_key_image_sync; + if(ver < 28) + return; + a & x.m_cold_key_images.parent(); + if(ver < 29) + return; + crypto::secret_key dummy_rpc_client_secret_key; // Compatibility for old RPC payment system + a & dummy_rpc_client_secret_key; + if(ver < 30) + { + x.m_has_ever_refreshed_from_node = false; + return; + } + a & x.m_has_ever_refreshed_from_node; +} +} // namespace serialization +} // namespace boost + +BOOST_CLASS_VERSION(wallet2_basic::hashchain, 0) +BOOST_CLASS_VERSION(wallet2_basic::transfer_details, 12) +BOOST_CLASS_VERSION(wallet2_basic::multisig_info::LR, 0) +BOOST_CLASS_VERSION(wallet2_basic::multisig_info, 1) +BOOST_CLASS_VERSION(wallet2_basic::unconfirmed_transfer_details, 8) +BOOST_CLASS_VERSION(wallet2_basic::confirmed_transfer_details, 6) +BOOST_CLASS_VERSION(wallet2_basic::payment_details, 5) +BOOST_CLASS_VERSION(wallet2_basic::pool_payment_details, 1) +BOOST_CLASS_VERSION(wallet2_basic::address_book_row, 18) +BOOST_CLASS_VERSION(wallet2_basic::cache, 30) + +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::hashchain) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::transfer_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::multisig_info::LR) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::multisig_info) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::unconfirmed_transfer_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::confirmed_transfer_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::payment_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::pool_payment_details) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::address_book_row) +BOOST_CLASS_EXPORT_KEY(::wallet2_basic::cache) diff --git a/src/wallet/wallet2_basic/wallet2_constants.h b/src/wallet/wallet2_basic/wallet2_constants.h new file mode 100644 index 0000000000..82623bca80 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_constants.h @@ -0,0 +1,42 @@ +// 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. + +#pragma once + +#include +#include + +namespace wallet2_basic +{ + // a minute and a half + constexpr uint32_t DEFAULT_INACTIVITY_LOCK_TIMEOUT = 90; + + constexpr size_t SUBADDRESS_LOOKAHEAD_MAJOR = 50; + constexpr size_t SUBADDRESS_LOOKAHEAD_MINOR = 200; + +} // namespace wallet2_basic diff --git a/src/wallet/wallet2_basic/wallet2_storage.cpp b/src/wallet/wallet2_basic/wallet2_storage.cpp new file mode 100644 index 0000000000..2d03ab0420 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_storage.cpp @@ -0,0 +1,859 @@ +// 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 +#include +#include +#include +#include + +#include "cryptonote_basic/account.h" +#include "device/device_cold.hpp" +#include "device_trezor/device_trezor.hpp" +#include "file_io_utils.h" +#include "serialization/binary_utils.h" +#include "storages/portable_storage_template_helper.h" +#include "wallet2_boost_serialization.h" +#include "wallet2_storage.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2_basic.storage" + +using namespace boost::archive; +using namespace tools; +using rapidjson::Document; + +#define TRY_NOFAIL(stmt) try { stmt; } catch (...) {} + +namespace +{ +struct cache_file_data +{ + crypto::chacha_iv iv; + std::string cache_data; + + BEGIN_SERIALIZE_OBJECT() + FIELD(iv) + FIELD(cache_data) + END_SERIALIZE() +}; + +struct keys_file_data +{ + crypto::chacha_iv iv; + std::string account_data; + + BEGIN_SERIALIZE_OBJECT() + FIELD(iv) + FIELD(account_data) + END_SERIALIZE() +}; + +static hw::i_device_callback noop_device_cb; + +// https://github.com/monero-project/monero/blob/67d190ce7c33602b6a3b804f633ee1ddb7fbb4a1/src/wallet/wallet2.cpp#L156 +static constexpr const char WALLET2_ASCII_OUTPUT_MAGIC[] = "MoneroAsciiDataV1"; + +template +wallet2_basic::cache boost_deserialize_cache(const std::string& cache_data) +{ + wallet2_basic::cache c; + std::istringstream iss(cache_data); + Archive ar(iss); + ar >> c; + return c; +} + +void save_pem_ascii_file(const std::string& path, const std::string& data) +{ + std::unique_ptr fp(fopen(path.c_str(), "w+"), &fclose); + CHECK_AND_ASSERT_THROW_MES(fp, + "Failed to open wallet file for writing: " << path << ": " << strerror(errno)); + + const unsigned char* const data_uc = reinterpret_cast(data.data()); + CHECK_AND_ASSERT_THROW_MES(PEM_write(fp.get(), WALLET2_ASCII_OUTPUT_MAGIC, "", data_uc, data.size()), + "Failed to PEM write to file: " << path); +} + +std::string load_pem_ascii_string(const std::string& pem_contents) +{ + std::unique_ptr bb(BIO_new_mem_buf(pem_contents.data(), pem_contents.size()), &BIO_free); + + char* name = NULL; + char* header = NULL; + unsigned char* data = NULL; + long data_len = 0; + const bool read_success = PEM_read_bio(bb.get(), &name, &header, &data, &data_len); + + std::string result_data; + bool alloc_success = false; + try + { + result_data = std::string((const char*) data, data_len); + alloc_success = true; + } + catch (...) {} + + OPENSSL_free((void *) name); + OPENSSL_free((void *) header); + OPENSSL_free((void *) data); + + CHECK_AND_ASSERT_THROW_MES(read_success, "Could not read string contents as PEM data"); + CHECK_AND_ASSERT_THROW_MES(alloc_success, "Could not allocate new result string from PEM read"); + + return result_data; +} + +/*************************************************************************************************** +********************************JSON ADAPTER HELPER FUNCTIONS*************************************** +***************************************************************************************************/ + +template void assign_when_mutable(T& dst, const U& src) { dst = src; } +template void assign_when_mutable(const T& dst, const U& src) {} + +template const T& as_const_ref(T& t) { return t; } + +template std::enable_if_t::value || std::is_enum::value> +adapt_json_field(T& out, const Document& json, const char* name, bool mand) +{ + const rapidjson::Value::ConstMemberIterator memb_it = json.FindMember(name); + if (memb_it != json.MemberEnd()) + { + if (memb_it->value.IsInt()) + out = static_cast(memb_it->value.GetInt()); + else if (memb_it->value.IsUint()) + out = static_cast(memb_it->value.GetUint()); + else if (memb_it->value.IsUint64()) + out = static_cast(memb_it->value.GetUint64()); + else + ASSERT_MES_AND_THROW("Field " << name << " found in JSON, but not an int-like number"); + } + else if (mand) + ASSERT_MES_AND_THROW("Field " << name << " not found in JSON"); +} + +void adapt_json_field(std::string& out, const Document& json, const char* name, bool mand) +{ + const rapidjson::Value::ConstMemberIterator memb_it = json.FindMember(name); + if (memb_it != json.MemberEnd()) + { + if (memb_it->value.IsString()) + out = std::string(memb_it->value.GetString(), memb_it->value.GetStringLength()); + else + ASSERT_MES_AND_THROW("Field " << name << " found in JSON, but not " << "String"); + } + else if (mand) + ASSERT_MES_AND_THROW("Field " << name << " not found in JSON"); +} + +// Load arbitrary types from JSON string fields represented in binary_archive format +template std::enable_if_t::value && !std::is_enum::value> +adapt_json_field(T& out, const Document& json, const char* name, bool mand) +{ + std::string binary_repr; + adapt_json_field(binary_repr, json, name, mand); + const bool r = serialization::parse_binary(binary_repr, out); + CHECK_AND_ASSERT_THROW_MES(r, "Could not parse object from binary archive in JSON field"); +} + +template std::enable_if_t::value || std::is_enum::value> +adapt_json_field(const T& in, Document& json, const char* name, bool) +{ + rapidjson::Value k(name, json.GetAllocator()); + rapidjson::Value v; + if (in < T{}) // Is negative? + v.SetInt(static_cast(in)); + else // Is positive + v.SetUint64(static_cast(in)); + json.AddMember(k, v, json.GetAllocator()); +} + +void adapt_json_field(const std::string& in, Document& json, const char* name, bool) +{ + rapidjson::Value k(name, json.GetAllocator()); + rapidjson::Value v(in.data(), in.size(), json.GetAllocator()); + json.AddMember(k, v, json.GetAllocator()); +} + +// Store arbitrary types to JSON string fields represented in binary_archive format +template std::enable_if_t::value && !std::is_enum::value> +adapt_json_field(const T& in, Document& json, const char* name, bool) +{ + std::string binary_repr; + const bool r = serialization::dump_binary(const_cast(in), binary_repr); + CHECK_AND_ASSERT_THROW_MES(r, "Could not represent object in binary archive"); + adapt_json_field(as_const_ref(binary_repr), json, name, true); +} + +template +void adapt_json_field(const T&, const Document&, const char*, bool) +{} + +} // anonymous namespace + +namespace wallet2_basic +{ +template +void adapt_keysdata_tofrom_json_object +( + detail::reference_mutate_enabled kd, + detail::reference_mutate_enabled obj, + const crypto::chacha_key& keys_key, + bool downgrade_to_watch_only +); + +/*************************************************************************************************** +*************************************CACHE STORAGE************************************************** +***************************************************************************************************/ +crypto::chacha_key cache::pwd_to_cache_key(const char* pwd, size_t len, uint64_t kdf_rounds) +{ + static_assert(crypto::HASH_SIZE == sizeof(crypto::chacha_key), "Mismatched sizes of hash and chacha key"); + + crypto::chacha_key key; + crypto::generate_chacha_key(pwd, len, key, kdf_rounds); + + epee::mlocked> cache_key_data; + memcpy(cache_key_data.data(), &key, crypto::HASH_SIZE); + cache_key_data[crypto::HASH_SIZE] = config::HASH_KEY_WALLET_CACHE; + crypto::cn_fast_hash(cache_key_data.data(), crypto::HASH_SIZE+1, reinterpret_cast(key)); + + return key; +} + +crypto::chacha_key cache::account_to_old_cache_key(const cryptonote::account_base& account, uint64_t kdf_rounds) +{ + crypto::chacha_key key; + hw::device &hwdev = account.get_device(); + const bool r = hwdev.generate_chacha_key(account.get_keys(), key, kdf_rounds); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "device failed to generate chacha key"); + return key; +} + +cache cache::load_from_memory +( + const std::string& cache_file_buf, + const epee::wipeable_string& password, + const cryptonote::account_base& wallet_account, + uint64_t kdf_rounds +) +{ + // Try to deserialize cache file buf into `cache_file_data` type. If success, + // then we are dealing with encrypted cache + cache_file_data cfd; + const bool encrypted_cache = ::serialization::parse_binary(cache_file_buf, cfd); + + if (encrypted_cache) + { + LOG_PRINT_L1("Taking encrypted wallet cache load path..."); + + // Decrypt cache contents into buffer + crypto::chacha_key cache_key = pwd_to_cache_key(password.data(), password.size(), kdf_rounds); + std::string cache_data; + cache_data.resize(cfd.cache_data.size()); + crypto::chacha20(cfd.cache_data.data(), cfd.cache_data.size(), cache_key, cfd.iv, &cache_data[0]); + + LOG_PRINT_L1("Trying to read from recent binary archive"); + try + { + cache c; + binary_archive ar{epee::strspan(cache_data)}; + if (::serialization::serialize(ar, c)) + if (::serialization::check_stream_state(ar)) + return c; + } + catch (...) {} + + LOG_PRINT_L1("Trying to read from binary archive with varint incompatibility"); + try + { + cache c; + binary_archive ar{epee::strspan(cache_data)}; + ar.enable_varint_bug_backward_compatibility(); + if (::serialization::serialize(ar, c)) + if (::serialization::check_stream_state(ar)) + return c; + } + catch (...) {} + + LOG_PRINT_L1("Trying to read from boost portable binary archive"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + + LOG_PRINT_L1("Switching to decryption key derived from account keys..."); + cache_key = account_to_old_cache_key(wallet_account, kdf_rounds); + crypto::chacha20(cfd.cache_data.data(), cfd.cache_data.size(), cache_key, cfd.iv, &cache_data[0]); + + LOG_PRINT_L1("Trying to read from boost portable binary archive encrypted with account keys"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + + LOG_PRINT_L1("Switching to old chacha8 encryption..."); + crypto::chacha8(cfd.cache_data.data(), cfd.cache_data.size(), cache_key, cfd.iv, &cache_data[0]); + + LOG_PRINT_L1("Trying to read from boost portable binary archive encrypted with account keys & chacha8"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + + LOG_PRINT_L1("Trying to read from boost UNportable binary archive encrypted with account keys & chacha8"); + TRY_NOFAIL(return boost_deserialize_cache(cache_data)); + } + else // not encrypted cache + { + LOG_PRINT_L1("Taking unencrypted wallet cache load path..."); + + LOG_PRINT_L1("Trying to read from boost portable binary archive unencrypted"); + TRY_NOFAIL(return boost_deserialize_cache(cache_file_buf)); + + LOG_PRINT_L1("Trying to read from boost UNportable binary archive unencrypted"); + TRY_NOFAIL(return boost_deserialize_cache(cache_file_buf)); + } + + THROW_WALLET_EXCEPTION(error::wallet_internal_error, "failed to load wallet cache"); +} + +std::string cache::store_to_memory(const epee::wipeable_string& password, uint64_t kdf_rounds) const +{ + return store_to_memory(pwd_to_cache_key(password.data(), password.size(), kdf_rounds)); +} + +std::string cache::store_to_memory(const crypto::chacha_key& encryption_key) const +{ + // Serialize cache + std::stringstream oss; + binary_archive ar1(oss); + THROW_WALLET_EXCEPTION_IF(!::serialization::serialize(ar1, const_cast(*this)), + error::wallet_internal_error, "Failed to serialize cache"); + + // Prepare outer cache_file_data data structure + std::string cache_pt = oss.str(); + cache_file_data cfd; + cfd.iv = crypto::rand(); + cfd.cache_data.resize(cache_pt.size()); + + // Encrypt cache + crypto::chacha20(cache_pt.data(), cache_pt.size(), encryption_key, cfd.iv, &cfd.cache_data[0]); + + // Serialize cache_file_data structure + oss.str(""); + binary_archive ar2(oss); + THROW_WALLET_EXCEPTION_IF(!::serialization::serialize(ar2, cfd), + error::wallet_internal_error, "Failed to serialize outer cache file data"); + + return oss.str(); +} + +/*************************************************************************************************** +*********************************WALLET KEYS STORAGE************************************************ +***************************************************************************************************/ + +crypto::chacha_key keys_data::pwd_to_keys_data_key(const char* pwd, size_t len, uint64_t kdf_rounds) +{ + crypto::chacha_key key; + crypto::generate_chacha_key(pwd, len, key, kdf_rounds); + return key; +} + +keys_data keys_data::load_from_memory +( + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cryptonote::network_type nettype, + uint64_t kdf_rounds +) +{ + const crypto::chacha_key encryption_key = pwd_to_keys_data_key(password.data(), password.size(), kdf_rounds); + return load_from_memory(keys_file_buf, encryption_key, nettype); +} + +keys_data keys_data::load_from_memory +( + const std::string& keys_file_buf, + const crypto::chacha_key& encryption_key, + cryptonote::network_type nettype +) +{ + // Deserialize encrypted data and IV into `keys_file_data` structure + keys_file_data kfd; + bool r = ::serialization::parse_binary(keys_file_buf, kfd); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "internal error: failed to deserialize keys buffer"); + + // Derive chacha decryption key from password and decrypt key buffer + std::string decrypted_keys_data; + decrypted_keys_data.resize(kfd.account_data.size()); + crypto::chacha20(kfd.account_data.data(), kfd.account_data.size(), encryption_key, kfd.iv, &decrypted_keys_data[0]); + + rapidjson::Document json; + if (json.Parse(decrypted_keys_data.c_str()).HasParseError() || !json.IsObject()) + crypto::chacha8(kfd.account_data.data(), kfd.account_data.size(), encryption_key, kfd.iv, &decrypted_keys_data[0]); + + keys_data kd; + kd.m_nettype = nettype; + + if (json.Parse(decrypted_keys_data.c_str()).HasParseError()) + { + CHECK_AND_ASSERT_THROW_MES(nettype != cryptonote::UNDEFINED, + "No network type was provided and we can't deduce nettype from old wallet keys files"); + kd.is_old_file_format = true; + r = epee::serialization::load_t_from_binary(kd.m_account, decrypted_keys_data); + THROW_WALLET_EXCEPTION_IF(!r, error::invalid_password); + } + else if (json.IsObject()) // The contents should be JSON if the wallet follows the new format. + { + adapt_keysdata_tofrom_json_object(kd, json, encryption_key, false); + } + else + { + THROW_WALLET_EXCEPTION(error::wallet_internal_error, + "malformed wallet keys JSON: Document root is not an object"); + } + + return kd; +} + +std::string keys_data::store_to_memory +( + const epee::wipeable_string& password, + bool downgrade_to_watch_only, + uint64_t kdf_rounds +) const +{ + const crypto::chacha_key encryption_key = pwd_to_keys_data_key(password.data(), password.size(), kdf_rounds); + return store_to_memory(encryption_key, downgrade_to_watch_only); +} + +std::string keys_data::store_to_memory +( + const crypto::chacha_key& encryption_key, + bool downgrade_to_watch_only +) const +{ + // Create JSON object containing all the information we need about our keys data + rapidjson::Document json; + json.SetObject(); + adapt_keysdata_tofrom_json_object(*this, json, encryption_key, downgrade_to_watch_only); + + // Serialize the JSON object + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + json.Accept(writer); + + // Encrypt the JSON buffer into a keys_file_data structure + keys_file_data kfd; + kfd.account_data.resize(buffer.GetSize()); + kfd.iv = crypto::rand(); + crypto::chacha20(buffer.GetString(), buffer.GetSize(), encryption_key, kfd.iv, &kfd.account_data[0]); + + // Serialize the keys_file_data structure as a binary archive + std::string final_buf; + const bool r = ::serialization::dump_binary(kfd, final_buf); + CHECK_AND_ASSERT_THROW_MES(r, "Failed to serialize keys_file_data into binary archive"); + + return final_buf; +} + +void keys_data::setup_account_keys_and_devices +( + const epee::wipeable_string& password, + hw::i_device_callback* device_cb, + uint64_t kdf_rounds +) +{ + const crypto::chacha_key encryption_key = pwd_to_keys_data_key(password.data(), password.size(), kdf_rounds); + setup_account_keys_and_devices(encryption_key, device_cb); +} + +void keys_data::setup_account_keys_and_devices +( + const crypto::chacha_key& encryption_key, + hw::i_device_callback* device_cb +) +{ + if (m_key_device_type == hw::device::device_type::LEDGER || m_key_device_type == hw::device::device_type::TREZOR) + { + LOG_PRINT_L0("Account on device. Initing device..."); + hw::device &hwdev = reconnect_device(device_cb); + + cryptonote::account_public_address device_account_public_address; + bool fetch_device_address = true; + + ::hw::device_cold* dev_cold = nullptr; + if (m_key_device_type == hw::device::device_type::TREZOR && (dev_cold = dynamic_cast<::hw::device_cold*>(&hwdev)) != nullptr) + { + THROW_WALLET_EXCEPTION_IF( + !dev_cold->get_public_address_with_no_passphrase(device_account_public_address), + error::wallet_internal_error, "Cannot get a device address"); + if (device_account_public_address == m_account.get_keys().m_account_address) + { + LOG_PRINT_L0("Wallet opened with an empty passphrase"); + fetch_device_address = false; + dev_cold->set_use_empty_passphrase(true); + } + else + { + fetch_device_address = true; + LOG_PRINT_L0("Wallet opening with an empty passphrase failed. Retry again: " << fetch_device_address); + dev_cold->reset_session(); + } + } + + if (fetch_device_address) + { + THROW_WALLET_EXCEPTION_IF(!hwdev.get_public_address(device_account_public_address), + error::wallet_internal_error, "Cannot get a device address"); + } + + THROW_WALLET_EXCEPTION_IF(device_account_public_address != m_account.get_keys().m_account_address, + error::wallet_internal_error, + "Device wallet does not match wallet address. If the device uses the passphrase feature, " + "please check whether the passphrase was entered correctly (it may have been misspelled - " + "different passphrases generate different wallets, passphrase is case-sensitive). " + "Device address: " + cryptonote::get_account_address_as_str(m_nettype, false, device_account_public_address) + + ", wallet address: " + m_account.get_public_address_str(m_nettype)); + LOG_PRINT_L0("Device inited..."); + } + else if (requires_external_device()) + { + THROW_WALLET_EXCEPTION(error::wallet_internal_error, "hardware device not supported"); + } + + hw::device& hwdev = m_account.get_keys().get_device(); + const bool view_only = m_watch_only || m_multisig || hwdev.device_protocol() == hw::device::PROTOCOL_COLD; + const bool keys_verified = verify_account_keys(view_only); + CHECK_AND_ASSERT_THROW_MES(keys_verified, "Device does not appear to correspond to this wallet file"); +} + +bool keys_data::verify_account_keys +( + bool view_only, + hw::device* alt_device +) const +{ + return wallet2_basic::verify_account_keys(m_account.get_keys(), view_only, alt_device); +} + +hw::device& keys_data::reconnect_device(hw::i_device_callback* device_cb) +{ +#ifdef WITH_DEVICE_TREZOR + hw::trezor::register_all(); +#endif + hw::device& hwdev = hw::get_device(m_device_name); + + THROW_WALLET_EXCEPTION_IF(!hwdev.set_name(m_device_name), error::wallet_internal_error, + "Could not set device name " + m_device_name); + hwdev.set_network_type(m_nettype); + hwdev.set_derivation_path(m_device_derivation_path); + hwdev.set_callback(device_cb ? device_cb : &noop_device_cb); + THROW_WALLET_EXCEPTION_IF(!hwdev.init(), error::wallet_internal_error, + "Could not initialize the device " + m_device_name); + THROW_WALLET_EXCEPTION_IF(!hwdev.connect(), error::wallet_internal_error, + "Could not connect to the device " + m_device_name); + m_account.set_device(hwdev); + + return hwdev; +} + +#define ADAPT_JSON_FIELD_N(name, jtype, mandatory, var) \ + do { \ + detail::reference_mutate_enabled, !SAVING> var_ref = var; \ + adapt_json_field(var_ref, obj, #name, mandatory); \ + } while (0); \ + +#define ADAPT_JSON_FIELD(name, jtype, mandatory) \ + ADAPT_JSON_FIELD_N(name, jtype, mandatory, kd.m_##name) \ + +template +void adapt_keysdata_tofrom_json_object +( + detail::reference_mutate_enabled kd, + detail::reference_mutate_enabled obj, + const crypto::chacha_key& keys_key, + bool downgrade_to_watch_only +) +{ + // Important prereq: we assume we already know obj is an object and not an array, number, etc + + // We always encrypt the account when storing now, but very old wallets didn't + bool account_keys_are_encrypted = SAVING; + ADAPT_JSON_FIELD_N(encrypted_secret_keys, Int, false, account_keys_are_encrypted); + assign_when_mutable(kd.m_keys_were_encrypted_on_load, account_keys_are_encrypted); + + if (SAVING) // Saving account to JSON + { + cryptonote::account_base encrypted_account = kd.m_account; + if (downgrade_to_watch_only) + encrypted_account.forget_spend_key(); + encrypted_account.encrypt_keys(keys_key); + const epee::byte_slice account_data_slice = epee::serialization::store_t_to_binary(encrypted_account); + const std::string account_data(reinterpret_cast(account_data_slice.data()), account_data_slice.size()); + ADAPT_JSON_FIELD_N(key_data, String, true, account_data); + } + else // Loading account from JSON + { + std::string account_data; + ADAPT_JSON_FIELD_N(key_data, String, true, account_data); + cryptonote::account_base decrypted_account; + CHECK_AND_ASSERT_THROW_MES( + epee::serialization::load_t_from_binary(decrypted_account, account_data), + "Could not parse account keys from EPEE binary"); + if (account_keys_are_encrypted) + decrypted_account.decrypt_keys(keys_key); + assign_when_mutable(kd.m_account, decrypted_account); + } + + ADAPT_JSON_FIELD(nettype, Uint, kd.m_nettype == cryptonote::UNDEFINED); + CHECK_AND_ASSERT_THROW_MES( + kd.m_nettype == cryptonote::MAINNET || + kd.m_nettype == cryptonote::TESTNET || + kd.m_nettype == cryptonote::STAGENET || + kd.m_nettype == cryptonote::FAKECHAIN, + "unrecognized network type for keys_data"); + + ADAPT_JSON_FIELD(multisig, Int, false); + ADAPT_JSON_FIELD(multisig_threshold, Uint, kd.m_multisig); + ADAPT_JSON_FIELD(multisig_rounds_passed, Uint, false); + ADAPT_JSON_FIELD(enable_multisig, Int, false); + ADAPT_JSON_FIELD(multisig_signers, binary_archive, kd.m_multisig); + ADAPT_JSON_FIELD(multisig_derivations, binary_archive, false); + + ADAPT_JSON_FIELD(watch_only, Int, false); + ADAPT_JSON_FIELD(confirm_non_default_ring_size, Int, false); + ADAPT_JSON_FIELD(ask_password, Int, false); // @TODO: Check AskPasswordType + ADAPT_JSON_FIELD(refresh_type, Int, false); // @TODO: Check RefreshType + ADAPT_JSON_FIELD(skip_to_height, Uint64, false); + ADAPT_JSON_FIELD(max_reorg_depth, Uint64, false); + ADAPT_JSON_FIELD(min_output_count, Uint, false); + ADAPT_JSON_FIELD(min_output_value, Uint64, false); + ADAPT_JSON_FIELD(merge_destinations, Int, false); + ADAPT_JSON_FIELD(confirm_backlog, Int, false); + ADAPT_JSON_FIELD(confirm_backlog_threshold, Uint, false); + ADAPT_JSON_FIELD(confirm_export_overwrite, Int, false); + ADAPT_JSON_FIELD(auto_low_priority, Int, false); + ADAPT_JSON_FIELD(confirm_export_overwrite, Int, false); + ADAPT_JSON_FIELD(segregate_pre_fork_outputs, Int, false); + ADAPT_JSON_FIELD(key_reuse_mitigation2, Int, false); + ADAPT_JSON_FIELD(segregation_height, Uint, false); + ADAPT_JSON_FIELD(ignore_fractional_outputs, Int, false); + ADAPT_JSON_FIELD(ignore_outputs_above, Uint64, false); + ADAPT_JSON_FIELD(ignore_outputs_below, Uint64, false); + ADAPT_JSON_FIELD(track_uses, Int, false); + ADAPT_JSON_FIELD(show_wallet_name_when_locked, Int, false); + ADAPT_JSON_FIELD(inactivity_lock_timeout, Uint, false); + ADAPT_JSON_FIELD(setup_background_mining, Int, false); + ADAPT_JSON_FIELD(subaddress_lookahead_major, Uint, false); + ADAPT_JSON_FIELD(subaddress_lookahead_minor, Uint, false); + ADAPT_JSON_FIELD(always_confirm_transfers, Int, false); + ADAPT_JSON_FIELD(print_ring_members, Int, false); + ADAPT_JSON_FIELD(store_tx_info, Int, false); + ADAPT_JSON_FIELD(default_mixin, Uint, false); + ADAPT_JSON_FIELD(export_format, Int, false); // @TODO Check ExportFormat + ADAPT_JSON_FIELD(load_deprecated_formats, Int, false); + ADAPT_JSON_FIELD(default_priority, Uint, false); + ADAPT_JSON_FIELD(auto_refresh, Int, false); + ADAPT_JSON_FIELD(device_derivation_path, String, false); + + ADAPT_JSON_FIELD_N(store_tx_keys, Int, false, kd.m_store_tx_info); // backward compat + ADAPT_JSON_FIELD_N(default_fee_multiplier, Uint, false, kd.m_default_priority); // backward compat + ADAPT_JSON_FIELD_N(refresh_height, Uint64, false, kd.m_refresh_from_block_height); + ADAPT_JSON_FIELD_N(key_on_device, Int, false, kd.m_key_device_type); + ADAPT_JSON_FIELD_N(seed_language, String, false, kd.seed_language); + + assign_when_mutable(kd.m_device_name, (kd.m_key_device_type == hw::device::device_type::LEDGER) ? "Ledger" : "default"); + ADAPT_JSON_FIELD(device_name, String, false); + + ADAPT_JSON_FIELD(original_keys_available, Int, false); + if (kd.m_original_keys_available) + { + std::string original_address, original_view_secret_key; + if (SAVING) + { + original_address = get_account_address_as_str(kd.m_nettype, false, kd.m_original_address); + ADAPT_JSON_FIELD_N(original_address, String, true, original_address); + original_view_secret_key = epee::string_tools::pod_to_hex(kd.m_original_view_secret_key); + ADAPT_JSON_FIELD_N(original_view_secret_key, String, true, original_view_secret_key); + } + else // loading original address + { + ADAPT_JSON_FIELD_N(original_address, String, true, original_address); + cryptonote::address_parse_info info; + CHECK_AND_ASSERT_THROW_MES(get_account_address_from_str(info, kd.m_nettype, original_address), + "Failed to parse original_address from JSON"); + assign_when_mutable(kd.m_original_address, info.address); + + ADAPT_JSON_FIELD_N(original_view_secret_key, String, true, original_view_secret_key); + crypto::secret_key original_view_secret_key_pod; + CHECK_AND_ASSERT_THROW_MES( + epee::string_tools::hex_to_pod(original_view_secret_key, original_view_secret_key_pod), + "Failed to parse original_view_secret_key from JSON"); + assign_when_mutable(kd.m_original_view_secret_key, original_view_secret_key_pod); + } + } +} + +/*************************************************************************************************** +********************************** MISC ACCOUNT UTILS ********************************************* +***************************************************************************************************/ + +bool verify_account_keys +( + const cryptonote::account_keys& keys, + bool view_only, + hw::device* hwdev +) +{ + if (nullptr == hwdev) + { + hwdev = std::addressof(keys.get_device()); + CHECK_AND_ASSERT_THROW_MES(hwdev, "Account device is NULL and no alternate was provided"); + } + + if (!hwdev->verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key)) + return false; + + if (!view_only) + if (!hwdev->verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key)) + return false; + + return true; +} + +/*************************************************************************************************** +********************* WALLET KEYS/CACHE COMBINATION LOADING/STORING ******************************** +***************************************************************************************************/ + +void load_keys_and_cache_from_memory +( + const std::string& cache_file_buf, + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype, + bool allow_external_devices_setup, + hw::i_device_callback* device_cb, + uint64_t kdf_rounds +) +{ + k = keys_data::load_from_memory(keys_file_buf, password, nettype, kdf_rounds); + if (!k.requires_external_device() || allow_external_devices_setup) + { + k.setup_account_keys_and_devices(password, device_cb, kdf_rounds); + } + c = cache::load_from_memory(cache_file_buf, password, k.m_account, kdf_rounds); +} + +void load_keys_and_cache_from_file +( + const std::string& cache_path, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype, + std::string keys_path, + bool allow_external_devices_setup, + hw::i_device_callback* device_cb, + uint64_t kdf_rounds +) +{ + if (keys_path.empty()) + { + keys_path = cache_path + ".keys"; + } + + std::string keys_file_contents; + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::load_file_to_string(keys_path, keys_file_contents), + "Could not load keys wallet file: " << keys_path); + + try + { + k = keys_data::load_from_memory(keys_file_contents, password, nettype, kdf_rounds); + } + catch (...) + { + keys_file_contents = load_pem_ascii_string(keys_file_contents); + k = keys_data::load_from_memory(keys_file_contents, password, nettype, kdf_rounds); + } + + if (!k.requires_external_device() || allow_external_devices_setup) + { + k.setup_account_keys_and_devices(password, device_cb, kdf_rounds); + } + + std::string cache_file_buf; + const bool loaded_cache = epee::file_io_utils::load_file_to_string(cache_path, cache_file_buf); + + if (loaded_cache) + { + c = cache::load_from_memory(cache_file_buf, password, k.m_account, kdf_rounds); + } + else + { + MWARNING("Could not load cache from filesystem, returning default cache"); + c = cache(); + } +} + +void store_keys_and_cache_to_memory +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + std::string& cache_buf, + std::string& keys_buf, + uint64_t kdf_rounds +) +{ + cache_buf = c.store_to_memory(password, kdf_rounds); + keys_buf = k.store_to_memory(password, false, kdf_rounds); +} + +void store_keys_and_cache_to_file +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + const std::string& cache_path, + uint64_t kdf_rounds, + ExportFormat keys_file_format +) +{ + const std::string keys_path = cache_path + ".keys"; + + std::string file_buf = c.store_to_memory(password, kdf_rounds); + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::save_string_to_file(cache_path, file_buf), + "could not save cache data to path '" << cache_path << "'"); + + file_buf = k.store_to_memory(password, false, kdf_rounds); + + if (keys_file_format == Binary) + { + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::save_string_to_file(keys_path, file_buf), + "could not save keys data to path '" << keys_path << "'"); + } + else // keys_file_format == Ascii + { + save_pem_ascii_file(keys_path, file_buf); + } +} +} // namespace wallet2_basic diff --git a/src/wallet/wallet2_basic/wallet2_storage.h b/src/wallet/wallet2_basic/wallet2_storage.h new file mode 100644 index 0000000000..e190ef3281 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_storage.h @@ -0,0 +1,330 @@ +// 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. + +/** + * @file utilities for loading and storing legacy wallet2 wallet data files, as well as handling device interaction + * + * Basic example of loading a wallet2 software wallet file: + * ```C++ + * const std::string wallet_filename = "testfile123"; + * const epee::wipeable_string wallet_password = "supersecret42"; + * + * wallet2_basic::cache example_cache; + * wallet2_basic::keys_data example_keys; + * + * wallet2_basic::load_keys_and_cache_from_file(wallet_filename, + wallet_password, + example_cache, + example_keys); + * + * std::cout << "my private view key is: " << + * epee::string_tools::pod_to_hex(example_keys.m_account.get_keys().m_view_secret_key) << std::endl; + * + * std::cout << "my transaction notes:" << std::endl; + * for (const auto& kv : example_cache.m_tx_notes) + * std::cout << " " << epee::string_tools::pod_to_hex(kv.first) << " - " << kv.second << std::endl; + * ``` +*/ + +#pragma once + +#include + +#include "wallet2_constants.h" +#include "wallet2_types.h" + +namespace wallet2_basic +{ +namespace detail +{ +template +using reference_mutate_enabled = std::conditional_t; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct cache +{ + hashchain m_blockchain; + transfer_container m_transfers; + cryptonote::account_public_address m_account_public_address; + serializable_unordered_map m_key_images; + serializable_unordered_map m_unconfirmed_txs; + payment_container m_payments; + serializable_unordered_map m_tx_keys; + serializable_unordered_map m_confirmed_txs; + serializable_unordered_map m_tx_notes; + serializable_unordered_multimap m_unconfirmed_payments; + serializable_unordered_map m_pub_keys; + std::vector m_address_book; + std::unordered_set m_scanned_pool_txs[2]; + serializable_unordered_map m_subaddresses; + std::vector> m_subaddress_labels; + serializable_unordered_map> m_additional_tx_keys; + serializable_unordered_map m_attributes; + std::pair, std::vector> m_account_tags; + bool m_ring_history_saved; + uint64_t m_last_block_reward; + // Aux transaction data from device + serializable_unordered_map m_tx_device; + uint64_t m_device_last_key_image_sync; + serializable_unordered_map m_cold_key_images; + bool m_has_ever_refreshed_from_node; + + // There's special key derivations for specifically wallet cache files for some reason + static crypto::chacha_key pwd_to_cache_key(const char* pwd, size_t len, uint64_t kdf_rounds = 1); + static crypto::chacha_key account_to_old_cache_key(const cryptonote::account_base& account, uint64_t kdf_rounds = 1); + + static cache load_from_memory + ( + const std::string& cache_file_buf, + const epee::wipeable_string& password, + const cryptonote::account_base& wallet_account, + uint64_t kdf_rounds = 1 + ); + + std::string store_to_memory(const epee::wipeable_string& password, uint64_t kdf_rounds = 1) const; + std::string store_to_memory(const crypto::chacha_key& encryption_key) const; + + BEGIN_SERIALIZE_OBJECT() + MAGIC_FIELD("monero wallet cache") + VERSION_FIELD(1) + FIELD(m_blockchain) + FIELD(m_transfers) + FIELD(m_account_public_address) + FIELD(m_key_images) + FIELD(m_unconfirmed_txs) + FIELD(m_payments) + FIELD(m_tx_keys) + FIELD(m_confirmed_txs) + FIELD(m_tx_notes) + FIELD(m_unconfirmed_payments) + FIELD(m_pub_keys) + FIELD(m_address_book) + FIELD(m_scanned_pool_txs[0]) + FIELD(m_scanned_pool_txs[1]) + FIELD(m_subaddresses) + FIELD(m_subaddress_labels) + FIELD(m_additional_tx_keys) + FIELD(m_attributes) + FIELD(m_account_tags) + FIELD(m_ring_history_saved) + FIELD(m_last_block_reward) + FIELD(m_tx_device) + FIELD(m_device_last_key_image_sync) + FIELD(m_cold_key_images) + crypto::secret_key dummy_rpc_client_secret_key; // Compatibility for old RPC payment system + FIELD_N("m_rpc_client_secret_key", dummy_rpc_client_secret_key) + if (version < 1) + { + m_has_ever_refreshed_from_node = false; + return true; + } + FIELD(m_has_ever_refreshed_from_node) + END_SERIALIZE() +}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +struct keys_data +{ + cryptonote::account_base m_account; + + bool is_old_file_format = false; + bool m_watch_only = false; /*!< no spend key */ + bool m_multisig = false; /*!< if > 1 spend secret key will not match spend public key */ + std::string seed_language = ""; /*!< Language of the mnemonics (seed). */ + cryptonote::network_type m_nettype = cryptonote::UNDEFINED; + uint32_t m_multisig_threshold = 0; + std::vector m_multisig_signers; + //in case of general M/N multisig wallet we should perform N - M + 1 key exchange rounds and remember how many rounds are passed. + uint32_t m_multisig_rounds_passed = 0; + std::vector m_multisig_derivations; + bool m_always_confirm_transfers = true; + bool m_print_ring_members = false; + bool m_store_tx_info = true; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ + uint32_t m_default_mixin = 0; + uint32_t m_default_priority = 0; + bool m_auto_refresh = true; + RefreshType m_refresh_type = RefreshDefault; + uint64_t m_refresh_from_block_height = 0; + uint64_t m_skip_to_height = 0; + // m_skip_to_height is useful when we don't want to modify the wallet's restore height. + // m_refresh_from_block_height is also a wallet's restore height which should remain constant unless explicitly modified by the user. + bool m_confirm_non_default_ring_size = true; + AskPasswordType m_ask_password = AskPasswordToDecrypt; + uint64_t m_max_reorg_depth = ORPHANED_BLOCKS_MAX_COUNT; + uint32_t m_min_output_count = 0; + uint64_t m_min_output_value = 0; + bool m_merge_destinations = false; + bool m_confirm_backlog = true; + uint32_t m_confirm_backlog_threshold = 0; + bool m_confirm_export_overwrite = true; + bool m_auto_low_priority = true; + bool m_segregate_pre_fork_outputs = true; + bool m_key_reuse_mitigation2 = true; + uint64_t m_segregation_height = 0; + bool m_ignore_fractional_outputs = true; + uint64_t m_ignore_outputs_above = MONEY_SUPPLY; + uint64_t m_ignore_outputs_below = 0; + bool m_track_uses = false; + bool m_show_wallet_name_when_locked = false; + uint32_t m_inactivity_lock_timeout = DEFAULT_INACTIVITY_LOCK_TIMEOUT; + BackgroundMiningSetupType m_setup_background_mining = BackgroundMiningMaybe; + size_t m_subaddress_lookahead_major = SUBADDRESS_LOOKAHEAD_MAJOR; + size_t m_subaddress_lookahead_minor = SUBADDRESS_LOOKAHEAD_MINOR; + bool m_original_keys_available = false; + cryptonote::account_public_address m_original_address; + crypto::secret_key m_original_view_secret_key; + ExportFormat m_export_format = ExportFormat::Binary; + bool m_load_deprecated_formats = false; + std::string m_device_name = ""; + std::string m_device_derivation_path = ""; + hw::device::device_type m_key_device_type = hw::device::device_type::SOFTWARE; + bool m_enable_multisig = false; + bool m_allow_mismatched_daemon_version = false; + + static crypto::chacha_key pwd_to_keys_data_key(const char* pwd, size_t len, uint64_t kdf_rounds = 1); + + static keys_data load_from_memory + ( + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cryptonote::network_type nettype = cryptonote::UNDEFINED, + uint64_t kdf_rounds = 1 + ); + static keys_data load_from_memory + ( + const std::string& keys_file_buf, + const crypto::chacha_key& encryption_key, + cryptonote::network_type nettype = cryptonote::UNDEFINED + ); + + std::string store_to_memory + ( + const epee::wipeable_string& password, + bool downgrade_to_watch_only = false, + uint64_t kdf_rounds = 1 + ) const; + std::string store_to_memory + ( + const crypto::chacha_key& encryption_key, + bool downgrade_to_watch_only = false + ) const; + + bool requires_external_device() const + { return m_key_device_type != hw::device::device_type::SOFTWARE; } + + bool keys_were_encrypted_on_load() const { return m_keys_were_encrypted_on_load; } + + void setup_account_keys_and_devices + ( + const epee::wipeable_string& password, + hw::i_device_callback* device_cb = nullptr, + uint64_t kdf_rounds = 1 + ); + + void setup_account_keys_and_devices + ( + const crypto::chacha_key& encryption_key, + hw::i_device_callback* device_cb = nullptr + ); + + bool verify_account_keys + ( + bool view_only = false, + hw::device* alt_device = nullptr + ) const; + + hw::device& reconnect_device(hw::i_device_callback* device_cb = nullptr); + +private: + bool m_keys_were_encrypted_on_load = false; + + template + friend void adapt_keysdata_tofrom_json_object + ( + detail::reference_mutate_enabled kd, + detail::reference_mutate_enabled obj, + const crypto::chacha_key& keys_key, + bool downgrade_to_watch_only + ); +}; // struct keys_data +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +bool verify_account_keys +( + const cryptonote::account_keys& keys, + bool view_only = false, + hw::device* alt_device = nullptr +); + +void load_keys_and_cache_from_memory +( + const std::string& cache_file_buf, + const std::string& keys_file_buf, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype = cryptonote::UNDEFINED, + bool allow_external_devices_setup = true, + hw::i_device_callback* device_cb = nullptr, + uint64_t kdf_rounds = 1 +); + +void load_keys_and_cache_from_file +( + const std::string& cache_path, + const epee::wipeable_string& password, + cache& c, + keys_data& k, + cryptonote::network_type nettype = cryptonote::UNDEFINED, + std::string keys_path = "", + bool allow_external_devices_setup = true, + hw::i_device_callback* device_cb = nullptr, + uint64_t kdf_rounds = 1 +); + +void store_keys_and_cache_to_memory +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + std::string& cache_buf, + std::string& keys_buf, + uint64_t kdf_rounds = 1 +); + +void store_keys_and_cache_to_file +( + const cache& c, + const keys_data& k, + const epee::wipeable_string& password, + const std::string& cache_path, + uint64_t kdf_rounds = 1, + ExportFormat keys_file_format = Binary +); +} // namespace wallet2_basic diff --git a/src/wallet/wallet2_basic/wallet2_types.h b/src/wallet/wallet2_basic/wallet2_types.h new file mode 100644 index 0000000000..e0205dbae4 --- /dev/null +++ b/src/wallet/wallet2_basic/wallet2_types.h @@ -0,0 +1,373 @@ +// 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. + +#pragma once + +#include "cryptonote_basic/cryptonote_basic.h" +#include "wallet/wallet_errors.h" + +namespace wallet2_basic +{ +struct HashchainAccessor; + +/** + * @brief: caches a contiguous list of block hashes and a genesis block +*/ +class hashchain +{ +public: + hashchain(): m_genesis(crypto::null_hash), m_offset(0) {} + + /** + * @brief: get the "height" of the blockchain, not the number of hashes stored + */ + size_t size() const { return m_blockchain.size() + m_offset; } + /** + * @brief: get the height that the hash list begins at + */ + size_t offset() const { return m_offset; } + /** + * @brief: get the genesis bloch hash + */ + const crypto::hash &genesis() const { return m_genesis; } + /** + * @brief: add a block hash to the top of the chain + */ + void push_back(const crypto::hash &hash) { if (m_offset == 0 && m_blockchain.empty()) m_genesis = hash; m_blockchain.push_back(hash); } + /** + * @brief: query if there is a hash available for a given height + */ + bool is_in_bounds(size_t idx) const { return idx >= m_offset && idx < size(); } + /** + * @brief: get a const reference to the block hash at a given height + */ + const crypto::hash &operator[](size_t idx) const { return m_blockchain[idx - m_offset]; } + /** + * @brief: get a mutable reference to the block hash at a given height + */ + crypto::hash &operator[](size_t idx) { return m_blockchain[idx - m_offset]; } + /** + * @brief: crop stored hashes after a certain height, where the height of the top block == `height`-1 + */ + void crop(size_t height) { m_blockchain.resize(std::max(std::min(height, size()), m_offset) - m_offset); } + /** + * @brief: delete all stored hashes and set the offset to 0 + */ + void clear() { m_offset = 0; m_blockchain.clear(); } + /** + * @brief: query if the blockchain is "empty": there are no stored hashes and the offset is 0 + */ + bool empty() const { return m_blockchain.empty() && m_offset == 0; } + /** + * @brief: crop stored hashes before a certain height and shift the offset accordingly, but always leave at least 1 hash + */ + void trim(size_t height) { while (height > m_offset && m_blockchain.size() > 1) { m_blockchain.pop_front(); ++m_offset; } m_blockchain.shrink_to_fit(); } + /** + * @brief: push a block hash onto the chain and move all block hashes back by one block + */ + void refill(const crypto::hash &hash) { m_blockchain.push_back(hash); --m_offset; } + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + VARINT_FIELD(m_offset) + FIELD(m_genesis) + FIELD(m_blockchain) + END_SERIALIZE() + +private: + size_t m_offset; + crypto::hash m_genesis; + std::deque m_blockchain; + + friend struct HashchainAccessor; +}; + +struct multisig_info +{ + struct LR + { + rct::key m_L; + rct::key m_R; + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_L) + FIELD(m_R) + END_SERIALIZE() + }; + + crypto::public_key m_signer; + std::vector m_LR; + std::vector m_partial_key_images; // one per key the participant has + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_signer) + FIELD(m_LR) + FIELD(m_partial_key_images) + END_SERIALIZE() +}; + +struct transfer_details +{ + uint64_t m_block_height; + cryptonote::transaction_prefix m_tx; + crypto::hash m_txid; + uint64_t m_internal_output_index; + uint64_t m_global_output_index; + bool m_spent; + bool m_frozen; + uint64_t m_spent_height; + crypto::key_image m_key_image; //TODO: key_image stored twice :( + rct::key m_mask; + uint64_t m_amount; + bool m_rct; + bool m_key_image_known; + bool m_key_image_request; // view wallets: we want to request it; cold wallets: it was requested + uint64_t m_pk_index; + cryptonote::subaddress_index m_subaddr_index; + bool m_key_image_partial; + std::vector m_multisig_k; + std::vector m_multisig_info; // one per other participant + std::vector> m_uses; + + bool is_rct() const { return m_rct; } + uint64_t amount() const { return m_amount; } + const crypto::public_key get_public_key() const { + crypto::public_key output_public_key; + THROW_WALLET_EXCEPTION_IF(m_tx.vout.size() <= m_internal_output_index, + tools::error::wallet_internal_error, "Too few outputs, outputs may be corrupted"); + THROW_WALLET_EXCEPTION_IF(!get_output_public_key(m_tx.vout[m_internal_output_index], output_public_key), + tools::error::wallet_internal_error, "Unable to get output public key from output"); + return output_public_key; + }; + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_block_height) + FIELD(m_tx) + FIELD(m_txid) + FIELD(m_internal_output_index) + FIELD(m_global_output_index) + FIELD(m_spent) + FIELD(m_frozen) + FIELD(m_spent_height) + FIELD(m_key_image) + FIELD(m_mask) + FIELD(m_amount) + FIELD(m_rct) + FIELD(m_key_image_known) + FIELD(m_key_image_request) + FIELD(m_pk_index) + FIELD(m_subaddr_index) + FIELD(m_key_image_partial) + FIELD(m_multisig_k) + FIELD(m_multisig_info) + FIELD(m_uses) + END_SERIALIZE() +}; + +typedef std::vector transfer_container; + +struct unconfirmed_transfer_details +{ + cryptonote::transaction_prefix m_tx; + uint64_t m_amount_in; + uint64_t m_amount_out; + uint64_t m_change; + time_t m_sent_time; + std::vector m_dests; + crypto::hash m_payment_id; + enum { pending, pending_in_pool, failed } m_state; + uint64_t m_timestamp; + uint32_t m_subaddr_account; // subaddress account of your wallet to be used in this transfer + std::set m_subaddr_indices; // set of address indices used as inputs in this transfer + std::vector>> m_rings; // relative + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(1) + FIELD(m_tx) + VARINT_FIELD(m_amount_in) + VARINT_FIELD(m_amount_out) + VARINT_FIELD(m_change) + VARINT_FIELD(m_sent_time) + FIELD(m_dests) + FIELD(m_payment_id) + if (version >= 1) + VARINT_FIELD(m_state) + VARINT_FIELD(m_timestamp) + VARINT_FIELD(m_subaddr_account) + FIELD(m_subaddr_indices) + FIELD(m_rings) + END_SERIALIZE() +}; + +struct confirmed_transfer_details +{ + cryptonote::transaction_prefix m_tx; + uint64_t m_amount_in; + uint64_t m_amount_out; + uint64_t m_change; + uint64_t m_block_height; + std::vector m_dests; + crypto::hash m_payment_id; + uint64_t m_timestamp; + uint64_t m_unlock_time; + uint32_t m_subaddr_account; // subaddress account of your wallet to be used in this transfer + std::set m_subaddr_indices; // set of address indices used as inputs in this transfer + std::vector>> m_rings; // relative + + confirmed_transfer_details() + : m_amount_in(0) + , m_amount_out(0) + , m_change((uint64_t)-1) + , m_block_height(0) + , m_payment_id(crypto::null_hash) + , m_timestamp(0) + , m_unlock_time(0) + , m_subaddr_account((uint32_t)-1) + {} + + confirmed_transfer_details(const unconfirmed_transfer_details &utd, uint64_t height) + : m_tx(utd.m_tx) + , m_amount_in(utd.m_amount_in) + , m_amount_out(utd.m_amount_out) + , m_change(utd.m_change) + , m_block_height(height) + , m_dests(utd.m_dests) + , m_payment_id(utd.m_payment_id) + , m_timestamp(utd.m_timestamp) + , m_unlock_time(utd.m_tx.unlock_time) + , m_subaddr_account(utd.m_subaddr_account) + , m_subaddr_indices(utd.m_subaddr_indices) + , m_rings(utd.m_rings) + {} + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(1) + if (version >= 1) + FIELD(m_tx) + VARINT_FIELD(m_amount_in) + VARINT_FIELD(m_amount_out) + VARINT_FIELD(m_change) + VARINT_FIELD(m_block_height) + FIELD(m_dests) + FIELD(m_payment_id) + VARINT_FIELD(m_timestamp) + VARINT_FIELD(m_unlock_time) + VARINT_FIELD(m_subaddr_account) + FIELD(m_subaddr_indices) + FIELD(m_rings) + END_SERIALIZE() +}; + +typedef std::vector amounts_container; +struct payment_details +{ + crypto::hash m_tx_hash; + uint64_t m_amount; + amounts_container m_amounts; + uint64_t m_fee; + uint64_t m_block_height; + uint64_t m_unlock_time; + uint64_t m_timestamp; + bool m_coinbase; + cryptonote::subaddress_index m_subaddr_index; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(m_tx_hash) + VARINT_FIELD(m_amount) + FIELD(m_amounts) + VARINT_FIELD(m_fee) + VARINT_FIELD(m_block_height) + VARINT_FIELD(m_unlock_time) + VARINT_FIELD(m_timestamp) + FIELD(m_coinbase) + FIELD(m_subaddr_index) + END_SERIALIZE() +}; + +typedef serializable_unordered_multimap payment_container; + +struct pool_payment_details +{ + payment_details m_pd; + bool m_double_spend_seen; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(m_pd) + FIELD(m_double_spend_seen) + END_SERIALIZE() +}; + +// GUI Address book +struct address_book_row +{ + cryptonote::account_public_address m_address; + crypto::hash8 m_payment_id; + std::string m_description; + bool m_is_subaddress; + bool m_has_payment_id; + + BEGIN_SERIALIZE_OBJECT() + VERSION_FIELD(0) + FIELD(m_address) + FIELD(m_payment_id) + FIELD(m_description) + FIELD(m_is_subaddress) + FIELD(m_has_payment_id) + END_SERIALIZE() +}; + +enum RefreshType +{ + RefreshFull, + RefreshOptimizeCoinbase, + RefreshNoCoinbase, + RefreshDefault = RefreshOptimizeCoinbase, +}; + +enum AskPasswordType +{ + AskPasswordNever = 0, + AskPasswordOnAction = 1, + AskPasswordToDecrypt = 2, +}; + +enum BackgroundMiningSetupType +{ + BackgroundMiningMaybe = 0, + BackgroundMiningYes = 1, + BackgroundMiningNo = 2, +}; + +enum ExportFormat +{ + Binary = 0, + Ascii, +}; +} // namespace wallet2_basic diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index a2b2484343..77a28b68c6 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -920,7 +920,7 @@ namespace tools #if !defined(_MSC_VER) template - void throw_wallet_ex(std::string&& loc, const TArgs&... args) + [[noreturn]] void throw_wallet_ex(std::string&& loc, const TArgs&... args) { TException e(std::move(loc), args...); LOG_PRINT_L0(e.to_string()); @@ -933,7 +933,7 @@ namespace tools #include template - void throw_wallet_ex(std::string&& loc) + [[noreturn]] void throw_wallet_ex(std::string&& loc) { TException e(std::move(loc)); LOG_PRINT_L0(e.to_string()); @@ -942,7 +942,7 @@ namespace tools #define GEN_throw_wallet_ex(z, n, data) \ template \ - void throw_wallet_ex(std::string&& loc, BOOST_PP_ENUM_BINARY_PARAMS(n, const TArg, &arg)) \ + [[noreturn]] void throw_wallet_ex(std::string&& loc, BOOST_PP_ENUM_BINARY_PARAMS(n, const TArg, &arg)) \ { \ TException e(std::move(loc), BOOST_PP_ENUM_PARAMS(n, arg)); \ LOG_PRINT_L0(e.to_string()); \ diff --git a/tests/functional_tests/functional_tests_rpc.py b/tests/functional_tests/functional_tests_rpc.py index 9975cdfa23..1be5fea3c2 100755 --- a/tests/functional_tests/functional_tests_rpc.py +++ b/tests/functional_tests/functional_tests_rpc.py @@ -86,6 +86,9 @@ print('Starting servers...') try: + # Setup directories + subprocess.Popen(['rm', '-rf', WALLET_DIRECTORY]) + PYTHONPATH = os.environ['PYTHONPATH'] if 'PYTHONPATH' in os.environ else '' if len(PYTHONPATH) > 0: PYTHONPATH += ':' diff --git a/tests/functional_tests/wallet.py b/tests/functional_tests/wallet.py index 3bb4459d6b..c0f4676f1b 100755 --- a/tests/functional_tests/wallet.py +++ b/tests/functional_tests/wallet.py @@ -301,6 +301,24 @@ def open_close(self): except: ok = True assert ok + #################################################################################################################### + # This create->close->open pattern reveals if you have code which performs loading correctly but storing incorrectly + + wallet.create_wallet('createcloseopen', password='NIST SP 800-90A -- Dual_EC_DRBG') + cco_addr = wallet.get_address().address + assert cco_addr != '44Kbx4sJ7JDRDV5aAhLJzQCjDz2ViLRduE3ijDZu3osWKBjMGkV1XPk4pfDUMqt1Aiezvephdqm6YD19GKFD9ZcXVUTp6BW' # what are the chances haha + + wallet.close_wallet() + ok = False + try: wallet.get_address() + except: ok = True + assert ok + + wallet.open_wallet('createcloseopen', password='NIST SP 800-90A -- Dual_EC_DRBG') + res = wallet.get_address() + assert res.address == cco_addr + #################################################################################################################### + wallet.restore_deterministic_wallet(seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted') res = wallet.get_address() assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm' diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index ddcf64f7f5..32b8acfa9d 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -113,6 +113,7 @@ set(unit_tests_sources output_selection.cpp vercmp.cpp ringdb.cpp + wallet_storage.cpp wipeable_string.cpp is_hdd.cpp aligned.cpp @@ -134,6 +135,7 @@ target_link_libraries(unit_tests cryptonote_core daemon_messages daemon_rpc_server + device_trezor blockchain_db lmdb_lib mx25519_static @@ -146,6 +148,7 @@ target_link_libraries(unit_tests seraphis_main seraphis_mocks wallet + wallet2_basic p2p version ${Boost_CHRONO_LIBRARY} diff --git a/tests/unit_tests/unit_tests_utils.h b/tests/unit_tests/unit_tests_utils.h index e3c6c2521f..2a452d21f8 100644 --- a/tests/unit_tests/unit_tests_utils.h +++ b/tests/unit_tests/unit_tests_utils.h @@ -72,3 +72,12 @@ namespace unit_test ASSERT_TRUE(found != map.end()); \ ASSERT_EQ(val, found->second); \ } while (false) + +#define EXPECT_EQ_MAP(val, map, key) \ + do { \ + auto found = map.find(key); \ + EXPECT_TRUE(found != map.end()); \ + if (found == map.end()) break; \ + EXPECT_EQ(val, found->second); \ + } while (false) \ + diff --git a/tests/unit_tests/wallet_storage.cpp b/tests/unit_tests/wallet_storage.cpp new file mode 100644 index 0000000000..27ceeaaa79 --- /dev/null +++ b/tests/unit_tests/wallet_storage.cpp @@ -0,0 +1,577 @@ +// 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 "gtest/gtest.h" + +#include "unit_tests_utils.h" +#include "wallet/wallet2_basic/wallet2_storage.h" +#include "wallet/wallet2.h" + +using namespace boost::filesystem; +using namespace epee::file_io_utils; + +static void check_wallet_9svHk1_key_contents(const tools::wallet2& w2, const tools::wallet2::ExportFormat export_format = tools::wallet2::Binary) +{ + // if wallet fails this first test, make sure that the wallet keys are decrypted + EXPECT_EQ("a16cc88f85ee9403bc642def92334ed203032ce91b060d353e6a532f47ff6200", epee::string_tools::pod_to_hex(w2.get_account().get_keys().m_spend_secret_key)); + EXPECT_EQ("339673bb1187e2f73ba7841ab6841c5553f96e9f13f8fe6612e69318db4e9d0a", epee::string_tools::pod_to_hex(w2.get_account().get_keys().m_view_secret_key)); + EXPECT_EQ(1483262038, w2.get_account().get_createtime()); + EXPECT_EQ(false, w2.is_deprecated()); // getter for member field is_old_file_format + EXPECT_EQ(false, w2.watch_only()); + + EXPECT_EQ(false, w2.multisig()); + EXPECT_EQ(false, w2.is_multisig_enabled()); + // @TODO: missing fields m_multisig_signers, m_multisig_rounds_passed, m_multisig_threshold, m_multisig_derivations + + EXPECT_EQ("English", w2.get_seed_language()); + EXPECT_EQ(cryptonote::TESTNET, w2.nettype()); + EXPECT_EQ(true, w2.always_confirm_transfers()); + EXPECT_EQ(false, w2.print_ring_members()); + EXPECT_EQ(true, w2.store_tx_info()); + EXPECT_EQ(0, w2.default_mixin()); + EXPECT_EQ(0, w2.get_default_priority()); + EXPECT_EQ(true, w2.auto_refresh()); + EXPECT_EQ(wallet2_basic::RefreshDefault, w2.get_refresh_type()); + EXPECT_EQ(818413, w2.get_refresh_from_block_height()); + // @TODO: missing m_skip_to_height + EXPECT_EQ(true, w2.confirm_non_default_ring_size()); + EXPECT_EQ(wallet2_basic::AskPasswordToDecrypt, w2.ask_password()); + EXPECT_EQ(ORPHANED_BLOCKS_MAX_COUNT, w2.max_reorg_depth()); + EXPECT_EQ(0, w2.get_min_output_count()); + EXPECT_EQ(0, w2.get_min_output_value()); + EXPECT_EQ(false, w2.merge_destinations()); + EXPECT_EQ(true, w2.confirm_backlog()); + EXPECT_EQ(0, w2.get_confirm_backlog_threshold()); + EXPECT_EQ(true, w2.confirm_export_overwrite()); + EXPECT_EQ(true, w2.auto_low_priority()); + EXPECT_EQ(true, w2.segregate_pre_fork_outputs()); + EXPECT_EQ(true, w2.key_reuse_mitigation2()); + EXPECT_EQ(0, w2.segregation_height()); + EXPECT_EQ(true, w2.ignore_fractional_outputs()); + EXPECT_EQ(MONEY_SUPPLY, w2.ignore_outputs_above()); + EXPECT_EQ(0, w2.ignore_outputs_below()); + EXPECT_EQ(false, w2.track_uses()); + EXPECT_EQ(false, w2.show_wallet_name_when_locked()); + EXPECT_EQ(wallet2_basic::DEFAULT_INACTIVITY_LOCK_TIMEOUT, w2.inactivity_lock_timeout()); + EXPECT_EQ(wallet2_basic::BackgroundMiningMaybe, w2.setup_background_mining()); + const std::pair exp_lookahead = {wallet2_basic::SUBADDRESS_LOOKAHEAD_MAJOR, wallet2_basic::SUBADDRESS_LOOKAHEAD_MINOR}; + EXPECT_EQ(exp_lookahead, w2.get_subaddress_lookahead()); + // @TODO: missing m_original_keys_available, m_original_address + EXPECT_EQ(export_format, w2.export_format()); + EXPECT_EQ(false, w2.load_deprecated_formats()); + EXPECT_EQ("default", w2.device_name()); + EXPECT_EQ("", w2.device_derivation_path()); + EXPECT_EQ(hw::device::device_type::SOFTWARE, w2.get_device_type()); + EXPECT_EQ(false, w2.is_mismatched_daemon_version_allowed()); +} + +static void check_wallet_9svHk1_key_contents(const wallet2_basic::keys_data& w2b, const wallet2_basic::ExportFormat export_format = wallet2_basic::Binary) +{ + // if wallet fails this first test, make sure that the wallet keys are decrypted + EXPECT_EQ("a16cc88f85ee9403bc642def92334ed203032ce91b060d353e6a532f47ff6200", epee::string_tools::pod_to_hex(w2b.m_account.get_keys().m_spend_secret_key)); + EXPECT_EQ("339673bb1187e2f73ba7841ab6841c5553f96e9f13f8fe6612e69318db4e9d0a", epee::string_tools::pod_to_hex(w2b.m_account.get_keys().m_view_secret_key)); + EXPECT_EQ(1483262038, w2b.m_account.get_createtime()); + EXPECT_EQ(false, w2b.is_old_file_format); // getter for member field is_old_file_format + EXPECT_EQ(false, w2b.m_watch_only); + + EXPECT_EQ(false, w2b.m_multisig); + EXPECT_EQ(false, w2b.m_enable_multisig); + // @TODO: missing fields m_multisig_signers, m_multisig_rounds_passed, m_multisig_threshold, m_multisig_derivations + + EXPECT_EQ("English", w2b.seed_language); + EXPECT_EQ(cryptonote::TESTNET, w2b.m_nettype); + EXPECT_EQ(true, w2b.m_always_confirm_transfers); + EXPECT_EQ(false, w2b.m_print_ring_members); + EXPECT_EQ(true, w2b.m_store_tx_info); + EXPECT_EQ(0, w2b.m_default_mixin); + EXPECT_EQ(0, w2b.m_default_priority); + EXPECT_EQ(true, w2b.m_auto_refresh); + EXPECT_EQ(wallet2_basic::RefreshDefault, w2b.m_refresh_type); + EXPECT_EQ(818413, w2b.m_refresh_from_block_height); + // @TODO: missing m_skip_to_height + EXPECT_EQ(true, w2b.m_confirm_non_default_ring_size); + EXPECT_EQ(wallet2_basic::AskPasswordToDecrypt, w2b.m_ask_password); + EXPECT_EQ(ORPHANED_BLOCKS_MAX_COUNT, w2b.m_max_reorg_depth); + EXPECT_EQ(0, w2b.m_min_output_count); + EXPECT_EQ(0, w2b.m_min_output_value); + EXPECT_EQ(false, w2b.m_merge_destinations); + EXPECT_EQ(true, w2b.m_confirm_backlog); + EXPECT_EQ(0, w2b.m_confirm_backlog_threshold); + EXPECT_EQ(true, w2b.m_confirm_export_overwrite); + EXPECT_EQ(true, w2b.m_auto_low_priority); + EXPECT_EQ(true, w2b.m_segregate_pre_fork_outputs); + EXPECT_EQ(true, w2b.m_key_reuse_mitigation2); + EXPECT_EQ(0, w2b.m_segregation_height); + EXPECT_EQ(true, w2b.m_ignore_fractional_outputs); + EXPECT_EQ(MONEY_SUPPLY, w2b.m_ignore_outputs_above); + EXPECT_EQ(0, w2b.m_ignore_outputs_below); + EXPECT_EQ(false, w2b.m_track_uses); + EXPECT_EQ(false, w2b.m_show_wallet_name_when_locked); + EXPECT_EQ(wallet2_basic::DEFAULT_INACTIVITY_LOCK_TIMEOUT, w2b.m_inactivity_lock_timeout); + EXPECT_EQ(wallet2_basic::BackgroundMiningMaybe, w2b.m_setup_background_mining); + EXPECT_EQ(wallet2_basic::SUBADDRESS_LOOKAHEAD_MAJOR, w2b.m_subaddress_lookahead_major); + EXPECT_EQ(wallet2_basic::SUBADDRESS_LOOKAHEAD_MINOR, w2b.m_subaddress_lookahead_minor); + // @TODO: missing m_original_keys_available, m_original_address + EXPECT_EQ(export_format, w2b.m_export_format); + EXPECT_EQ(false, w2b.m_load_deprecated_formats); + EXPECT_EQ("default", w2b.m_device_name); + EXPECT_EQ("", w2b.m_device_derivation_path); + EXPECT_EQ(hw::device::device_type::SOFTWARE, w2b.m_key_device_type); + EXPECT_EQ(false, w2b.m_allow_mismatched_daemon_version); +} + +namespace tools +{ +/*static*/ void check_wallet_9svHk1_cache_contents(const tools::wallet2& w2) +{ + /* + fields of tools::wallet2 to be checked: + std::vector m_blockchain + std::vector m_transfers // TODO + cryptonote::account_public_address m_account_public_address + std::unordered_map m_key_images + std::unordered_map m_unconfirmed_txs + std::unordered_multimap m_payments + std::unordered_map m_tx_keys + std::unordered_map m_confirmed_txs + std::unordered_map m_tx_notes + std::unordered_map m_unconfirmed_payments + std::unordered_map m_pub_keys + std::vector m_address_book + */ + // blockchain + ASSERT_TRUE(w2.m_blockchain.size() == 1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(w2.m_blockchain[0]) == "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b"); + // transfers (TODO) + EXPECT_TRUE(w2.m_transfers.size() == 3); + // account public address + EXPECT_TRUE(epee::string_tools::pod_to_hex(w2.m_account_public_address.m_view_public_key) == "e47d4b6df6ab7339539148c2a03ad3e2f3434e5ab2046848e1f21369a3937cad"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(w2.m_account_public_address.m_spend_public_key) == "13daa2af00ad26a372d317195de0bdd716f7a05d33bc4d7aff1664b6ee93c060"); + // key images + ASSERT_TRUE(w2.m_key_images.size() == 3); + { + crypto::key_image ki[3]; + epee::string_tools::hex_to_pod("c5680d3735b90871ca5e3d90cd82d6483eed1151b9ab75c2c8c3a7d89e00a5a8", ki[0]); + epee::string_tools::hex_to_pod("d54cbd435a8d636ad9b01b8d4f3eb13bd0cf1ce98eddf53ab1617f9b763e66c0", ki[1]); + epee::string_tools::hex_to_pod("6c3cd6af97c4070a7aef9b1344e7463e29c7cd245076fdb65da447a34da3ca76", ki[2]); + EXPECT_EQ_MAP(0, w2.m_key_images, ki[0]); + EXPECT_EQ_MAP(1, w2.m_key_images, ki[1]); + EXPECT_EQ_MAP(2, w2.m_key_images, ki[2]); + } + // unconfirmed txs + EXPECT_TRUE(w2.m_unconfirmed_txs.size() == 0); + // payments + ASSERT_TRUE(w2.m_payments.size() == 2); + { + auto pd0 = w2.m_payments.begin(); + auto pd1 = pd0; + ++pd1; + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + if (epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc") + swap(pd0, pd1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc"); + EXPECT_TRUE(pd0->second.m_amount == 13400845012231); + EXPECT_TRUE(pd1->second.m_amount == 1200000000000); + EXPECT_TRUE(pd0->second.m_block_height == 818424); + EXPECT_TRUE(pd1->second.m_block_height == 818522); + EXPECT_TRUE(pd0->second.m_unlock_time == 818484); + EXPECT_TRUE(pd1->second.m_unlock_time == 0); + EXPECT_TRUE(pd0->second.m_timestamp == 1483263366); + EXPECT_TRUE(pd1->second.m_timestamp == 1483272963); + } + // tx keys + ASSERT_TRUE(w2.m_tx_keys.size() == 2); + { + const std::vector> txid_txkey = + { + {"b9aac8c020ab33859e0c0b6331f46a8780d349e7ac17b067116e2d87bf48daad", "bf3614c6de1d06c09add5d92a5265d8c76af706f7bc6ac830d6b0d109aa87701"}, + {"6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", "e556884246df5a787def6732c6ea38f1e092fa13e5ea98f732b99c07a6332003"}, + }; + for (size_t i = 0; i < txid_txkey.size(); ++i) + { + crypto::hash txid; + crypto::secret_key txkey; + epee::string_tools::hex_to_pod(txid_txkey[i].first, txid); + epee::string_tools::hex_to_pod(txid_txkey[i].second, txkey); + EXPECT_EQ_MAP(txkey, w2.m_tx_keys, txid); + } + } + // confirmed txs + EXPECT_TRUE(w2.m_confirmed_txs.size() == 1); + // tx notes + ASSERT_TRUE(w2.m_tx_notes.size() == 2); + { + crypto::hash h[2]; + epee::string_tools::hex_to_pod("15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e", h[0]); + epee::string_tools::hex_to_pod("6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", h[1]); + EXPECT_EQ_MAP("sample note", w2.m_tx_notes, h[0]); + EXPECT_EQ_MAP("sample note 2", w2.m_tx_notes, h[1]); + } + // unconfirmed payments + EXPECT_TRUE(w2.m_unconfirmed_payments.size() == 0); + // pub keys + ASSERT_TRUE(w2.m_pub_keys.size() == 3); + { + crypto::public_key pubkey[3]; + epee::string_tools::hex_to_pod("33f75f264574cb3a9ea5b24220a5312e183d36dc321c9091dfbb720922a4f7b0", pubkey[0]); + epee::string_tools::hex_to_pod("5066ff2ce9861b1d131cf16eeaa01264933a49f28242b97b153e922ec7b4b3cb", pubkey[1]); + epee::string_tools::hex_to_pod("0d8467e16e73d16510452b78823e082e05ee3a63788d40de577cf31eb555f0c8", pubkey[2]); + EXPECT_EQ_MAP(0, w2.m_pub_keys, pubkey[0]); + EXPECT_EQ_MAP(1, w2.m_pub_keys, pubkey[1]); + EXPECT_EQ_MAP(2, w2.m_pub_keys, pubkey[2]); + } + // address book + ASSERT_TRUE(w2.m_address_book.size() == 1); + { + auto address_book_row = w2.m_address_book.begin(); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_spend_public_key) == "9bc53a6ff7b0831c9470f71b6b972dbe5ad1e8606f72682868b1dda64e119fb3"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_view_public_key) == "49fece1ef97dc0c0f7a5e2106e75e96edd910f7e86b56e1e308cd0cf734df191"); + EXPECT_TRUE(address_book_row->m_description == "testnet wallet 9y52S6"); + } +} +} // namespace tools + +static void check_wallet_9svHk1_cache_contents(const wallet2_basic::cache& c) +{ + /* + This test suite is adapated from unit test Serialization.portability_wallet + Cache fields to be checked: + std::vector m_blockchain + std::vector m_transfers + cryptonote::account_public_address m_account_public_address + std::unordered_map m_key_images + std::unordered_map m_unconfirmed_txs + std::unordered_multimap m_payments + std::unordered_map m_tx_keys + std::unordered_map m_confirmed_txs + std::unordered_map m_tx_notes + std::unordered_map m_unconfirmed_payments + std::unordered_map m_pub_keys + std::vector m_address_book + */ + + // blockchain + EXPECT_TRUE(c.m_blockchain.size() == 1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(c.m_blockchain[0]) == "48ca7cd3c8de5b6a4d53d2861fbdaedca141553559f9be9520068053cda8430b"); + // transfers (TODO) + EXPECT_TRUE(c.m_transfers.size() == 3); + // account public address + EXPECT_TRUE(epee::string_tools::pod_to_hex(c.m_account_public_address.m_view_public_key) == "e47d4b6df6ab7339539148c2a03ad3e2f3434e5ab2046848e1f21369a3937cad"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(c.m_account_public_address.m_spend_public_key) == "13daa2af00ad26a372d317195de0bdd716f7a05d33bc4d7aff1664b6ee93c060"); + // key images + ASSERT_TRUE(c.m_key_images.size() == 3); + { + crypto::key_image ki[3]; + epee::string_tools::hex_to_pod("c5680d3735b90871ca5e3d90cd82d6483eed1151b9ab75c2c8c3a7d89e00a5a8", ki[0]); + epee::string_tools::hex_to_pod("d54cbd435a8d636ad9b01b8d4f3eb13bd0cf1ce98eddf53ab1617f9b763e66c0", ki[1]); + epee::string_tools::hex_to_pod("6c3cd6af97c4070a7aef9b1344e7463e29c7cd245076fdb65da447a34da3ca76", ki[2]); + EXPECT_EQ_MAP(0, c.m_key_images, ki[0]); + EXPECT_EQ_MAP(1, c.m_key_images, ki[1]); + EXPECT_EQ_MAP(2, c.m_key_images, ki[2]); + } + // unconfirmed txs + EXPECT_TRUE(c.m_unconfirmed_txs.size() == 0); + // payments + ASSERT_TRUE(c.m_payments.size() == 2); + { + auto pd0 = c.m_payments.begin(); + auto pd1 = pd0; + ++pd1; + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->first) == "0000000000000000000000000000000000000000000000000000000000000000"); + if (epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc") + swap(pd0, pd1); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd0->second.m_tx_hash) == "15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(pd1->second.m_tx_hash) == "ec34c9bb12b99af33d49691384eee5bed9171498ff04e59516505f35d1fc5efc"); + EXPECT_TRUE(pd0->second.m_amount == 13400845012231); + EXPECT_TRUE(pd1->second.m_amount == 1200000000000); + EXPECT_TRUE(pd0->second.m_block_height == 818424); + EXPECT_TRUE(pd1->second.m_block_height == 818522); + EXPECT_TRUE(pd0->second.m_unlock_time == 818484); + EXPECT_TRUE(pd1->second.m_unlock_time == 0); + EXPECT_TRUE(pd0->second.m_timestamp == 1483263366); + EXPECT_TRUE(pd1->second.m_timestamp == 1483272963); + } + // tx keys + ASSERT_TRUE(c.m_tx_keys.size() == 2); + { + const std::vector> txid_txkey = + { + {"b9aac8c020ab33859e0c0b6331f46a8780d349e7ac17b067116e2d87bf48daad", "bf3614c6de1d06c09add5d92a5265d8c76af706f7bc6ac830d6b0d109aa87701"}, + {"6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", "e556884246df5a787def6732c6ea38f1e092fa13e5ea98f732b99c07a6332003"}, + }; + for (size_t i = 0; i < txid_txkey.size(); ++i) + { + crypto::hash txid; + crypto::secret_key txkey; + epee::string_tools::hex_to_pod(txid_txkey[i].first, txid); + epee::string_tools::hex_to_pod(txid_txkey[i].second, txkey); + EXPECT_EQ_MAP(txkey, c.m_tx_keys, txid); + } + } + // confirmed txs + EXPECT_TRUE(c.m_confirmed_txs.size() == 1); + // tx notes + ASSERT_TRUE(c.m_tx_notes.size() == 2); + { + crypto::hash h[2]; + epee::string_tools::hex_to_pod("15024343b38e77a1a9860dfed29921fa17e833fec837191a6b04fa7cb9605b8e", h[0]); + epee::string_tools::hex_to_pod("6e7013684d35820f66c6679197ded9329bfe0e495effa47e7b25258799858dba", h[1]); + EXPECT_EQ_MAP("sample note", c.m_tx_notes, h[0]); + EXPECT_EQ_MAP("sample note 2", c.m_tx_notes, h[1]); + } + // unconfirmed payments + EXPECT_TRUE(c.m_unconfirmed_payments.size() == 0); + // pub keys + ASSERT_TRUE(c.m_pub_keys.size() == 3); + { + crypto::public_key pubkey[3]; + epee::string_tools::hex_to_pod("33f75f264574cb3a9ea5b24220a5312e183d36dc321c9091dfbb720922a4f7b0", pubkey[0]); + epee::string_tools::hex_to_pod("5066ff2ce9861b1d131cf16eeaa01264933a49f28242b97b153e922ec7b4b3cb", pubkey[1]); + epee::string_tools::hex_to_pod("0d8467e16e73d16510452b78823e082e05ee3a63788d40de577cf31eb555f0c8", pubkey[2]); + EXPECT_EQ_MAP(0, c.m_pub_keys, pubkey[0]); + EXPECT_EQ_MAP(1, c.m_pub_keys, pubkey[1]); + EXPECT_EQ_MAP(2, c.m_pub_keys, pubkey[2]); + } + // address book + ASSERT_TRUE(c.m_address_book.size() == 1); + { + auto address_book_row = c.m_address_book.begin(); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_spend_public_key) == "9bc53a6ff7b0831c9470f71b6b972dbe5ad1e8606f72682868b1dda64e119fb3"); + EXPECT_TRUE(epee::string_tools::pod_to_hex(address_book_row->m_address.m_view_public_key) == "49fece1ef97dc0c0f7a5e2106e75e96edd910f7e86b56e1e308cd0cf734df191"); + EXPECT_TRUE(address_book_row->m_description == "testnet wallet 9y52S6"); + } +} + +TEST(wallet_storage, legacy_load_sanity) +{ + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + tools::wallet2 w2(cryptonote::TESTNET, 1, true); + w2.load(original_wallet_file.string(), password); + + check_wallet_9svHk1_cache_contents(w2); + check_wallet_9svHk1_key_contents(w2); +} + +TEST(wallet_storage, read_old_wallet) +{ + const boost::filesystem::path wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + wallet2_basic::cache c; + wallet2_basic::keys_data k; + wallet2_basic::load_keys_and_cache_from_file(wallet_file.string(), password, c, k); + + check_wallet_9svHk1_cache_contents(c); + check_wallet_9svHk1_key_contents(k); +} + +TEST(wallet_storage, backwards_compatible_store_file) +{ + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + const boost::filesystem::path target_wallet_file = unit_test::data_dir / "wallet_9svHk1_backwards_compatible_store_file"; + + wallet2_basic::cache c; + wallet2_basic::keys_data k; + + // load then save to target_wallet_file + wallet2_basic::load_keys_and_cache_from_file + ( + original_wallet_file.string(), + password, + c, + k + ); + wallet2_basic::store_keys_and_cache_to_file + ( + c, + k, + password, + target_wallet_file.string() + ); + + tools::wallet2 w2(cryptonote::TESTNET, 1, true); + w2.load(target_wallet_file.string(), password); // load the new file created by wallet2_basic + + check_wallet_9svHk1_cache_contents(w2); + check_wallet_9svHk1_key_contents(w2); +} + +TEST(wallet_storage, back_compat_ascii_format) +{ + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const boost::filesystem::path intermediate_wallet_file = unit_test::data_dir / "wallet_9svHk1_back_compat_ascii_load"; + const boost::filesystem::path final_wallet_file = unit_test::data_dir / "wallet_9svHk1_back_compat_ascii_load_w2b"; + const epee::wipeable_string password = "test"; + + copy_file(original_wallet_file, intermediate_wallet_file, copy_option::overwrite_if_exists); + copy_file(original_wallet_file.string() + ".keys", intermediate_wallet_file.string() + ".keys", copy_option::overwrite_if_exists); + + { + tools::wallet2 w(cryptonote::TESTNET, 1, true); + w.load(intermediate_wallet_file.string(), password); + w.set_export_format(tools::wallet2::Ascii); + w.store(); + w.rewrite(intermediate_wallet_file.string(), password); + } + + { + wallet2_basic::cache c; + wallet2_basic::keys_data k; + wallet2_basic::load_keys_and_cache_from_file + ( + intermediate_wallet_file.string(), + password, + c, + k + ); + + check_wallet_9svHk1_cache_contents(c); + check_wallet_9svHk1_key_contents(k, wallet2_basic::Ascii); + + wallet2_basic::store_keys_and_cache_to_file + ( + c, + k, + password, + final_wallet_file.string(), + 1, + wallet2_basic::Ascii + ); + } + + { + tools::wallet2 w(cryptonote::TESTNET, 1, true); + w.set_export_format(tools::wallet2::Ascii); + w.load(final_wallet_file.string(), password); + + check_wallet_9svHk1_cache_contents(w); + check_wallet_9svHk1_key_contents(w, tools::wallet2::Ascii); + } +} + +TEST(wallet_storage, back_compat_kdf_rounds) +{ + static constexpr uint64_t const KDF_ROUNDS_TEST_MIN = 2; + static constexpr uint64_t const KDF_ROUNDS_TEST_MAX = 8; + static constexpr uint64_t const KDF_ROUNDS_TEST_STEP = 3; + + const boost::filesystem::path original_wallet_file = unit_test::data_dir / "wallet_9svHk1"; + const epee::wipeable_string password = "test"; + + for (uint64_t kdf_rounds = KDF_ROUNDS_TEST_MIN; kdf_rounds <= KDF_ROUNDS_TEST_MAX; kdf_rounds += KDF_ROUNDS_TEST_STEP) + { + const boost::filesystem::path target_wallet_file = unit_test::data_dir / ("wallet_9svHk1_back_compat_kdf_rounds_" + std::to_string(kdf_rounds)); + + wallet2_basic::cache c; + wallet2_basic::keys_data k; + + // load then save to target_wallet_file + wallet2_basic::load_keys_and_cache_from_file + ( + original_wallet_file.string(), + password, + c, + k + ); + wallet2_basic::store_keys_and_cache_to_file + ( + c, + k, + password, + target_wallet_file.string(), + kdf_rounds /// <----- non-standard KDF rounds + ); + + tools::wallet2 w2(cryptonote::TESTNET, kdf_rounds, true); /// <----- non-standard KDF rounds + w2.load(target_wallet_file.string(), password); // load the new file created by wallet2_basic + + check_wallet_9svHk1_cache_contents(w2); + check_wallet_9svHk1_key_contents(w2); + } +} + +TEST(wallet_storage, load_multiple_kdf_rounds) +{ + const boost::filesystem::path wallet_file = unit_test::data_dir / "wallet_load_non_standard_kdf_rounds"; + const uint32_t kdf_rounds = 2 + crypto::rand_idx(10); // kdf_rounds in [2, 11] + const epee::wipeable_string password("88 FR 72701"); + const crypto::hash random_txid = crypto::rand(); + const std::string txid_note = "note for txid ;)"; + + cryptonote::account_base acc1, acc2; + + if (exists(wallet_file)) + remove(wallet_file); + if (exists(wallet_file.string() + ".keys")) + remove(wallet_file.string() + ".keys"); + + { + tools::wallet2 w(cryptonote::STAGENET, kdf_rounds, true); + w.generate(wallet_file.string(), password); + acc1 = w.get_account(); + w.set_tx_note(random_txid, txid_note); + w.store(); + } + + { + wallet2_basic::cache c; + wallet2_basic::keys_data k; + + wallet2_basic::load_keys_and_cache_from_file + ( + wallet_file.string(), + password, + c, + k, + cryptonote::UNDEFINED, + "", + false, + nullptr, + kdf_rounds + ); + + acc2 = k.m_account; + + ASSERT_TRUE(c.m_tx_notes.find(random_txid) != c.m_tx_notes.cend()); + EXPECT_EQ(txid_note, c.m_tx_notes[random_txid]); + } + + ASSERT_NE(crypto::secret_key{}, acc1.get_keys().m_spend_secret_key); + ASSERT_NE(crypto::secret_key{}, acc2.get_keys().m_spend_secret_key); + + EXPECT_EQ(acc1.get_keys().m_view_secret_key, acc2.get_keys().m_view_secret_key); + EXPECT_EQ(acc1.get_keys().m_spend_secret_key, acc2.get_keys().m_spend_secret_key); + EXPECT_EQ(acc1.get_createtime(), acc2.get_createtime()); +}