diff --git a/include/lws_frontend.h b/include/lws_frontend.h index c9019ab..b59ba4b 100644 --- a/include/lws_frontend.h +++ b/include/lws_frontend.h @@ -37,7 +37,7 @@ namespace lwsf { std::string displayAmount(std::uint64_t amount); - std::optional amountFromString(const std::string &amount); + Monero::optional amountFromString(const std::string &amount); bool addressValid(const std::string &str, Monero::NetworkType nettype); std::vector> qrcode(Monero::Wallet const* wal, std::uint32_t major, std::uint32_t minor); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e5738c7..874bb5a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -57,12 +57,29 @@ set(lwsf_headers ) add_library(lwsf-api ${lwsf_sources} ${lwsf_headers}) + +set(LWSF_COMPILE_DEFINITIONS) +if (${LWSF_MASTER_ENABLE}) + list(APPEND LWSF_COMPILE_DEFINITIONS "LWSF_MASTER_ENABLE") +endif() + +set(LWSF_POLYSEED_INCLUDE "") +set(LWSF_POLYSEED_LIBRARY "") +if (NOT ("${LWSF_POLYSEED_SOURCE}" STREQUAL "")) + list(APPEND LWSF_COMPILE_DEFINITIONS "LWSF_POLYSEED_ENABLE") + set(LWSF_POLYSEED_INCLUDE "${LWSF_POLYSEED_SOURCE}/include") + set(LWSF_POLYSEED_LIBRARY polyseed) +endif() + +target_compile_definitions(lwsf-api PRIVATE ${LWSF_COMPILE_DEFINITIONS}) target_include_directories(lwsf-api PUBLIC "${lwsf_SOURCE_DIR}/include" PRIVATE + ${LWSF_POLYSEED_INCLUDE} "." ) + target_link_libraries(lwsf-api PUBLIC monero::libraries @@ -73,12 +90,10 @@ target_link_libraries(lwsf-api lwsf-wire-wrapper ${Boost_THREAD_LIBRARY} ${sodium_LIBRARY_RELEASE} + ${LWSF_POLYSEED_LIBRARY} Threads::Threads ) -if (${LWSF_MASTER_ENABLE}) - target_compile_definitions(lwsf-api PRIVATE LWSF_MASTER_ENABLE) -endif() add_executable(lwsf-ledger ledger_main.cpp) target_link_libraries(lwsf-ledger diff --git a/src/backend.cpp b/src/backend.cpp index 1ae6f67..eacc35b 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -277,6 +277,7 @@ namespace lwsf { namespace internal { namespace backend { wire::object(format, WIRE_FIELD(language), + WIRE_OPTIONAL_FIELD(poly), wire::optional_field("addressbook", wire::trusted_array(std::ref(self.addressbook))), wire::optional_field("subaccounts", wire::trusted_array(std::ref(self.subaccounts))), wire::optional_field("txes", wire::trusted_array(std::ref(self.txes))), @@ -292,6 +293,12 @@ namespace lwsf { namespace internal { namespace backend ); } + template + void map_polyseed(F& format, T& self) + { + wire::object(format, WIRE_FIELD(seed), WIRE_FIELD(passphrase)); + } + rct::key get_mask(const crypto::secret_key& view_key, const rpc::output& source) { crypto::key_derivation derived{}; @@ -719,6 +726,7 @@ namespace lwsf { namespace internal { namespace backend WIRE_DEFINE_OBJECT(transfer_out, map_transfer_out); WIRE_DEFINE_OBJECT(transaction, map_transaction); WIRE_DEFINE_OBJECT(keypair, map_keypair); + WIRE_DEFINE_OBJECT(account::polyseed, map_polyseed); void read_bytes(wire::reader& source, account& dest) { map_account(source, dest); @@ -892,9 +900,10 @@ namespace lwsf { namespace internal { namespace backend return status; } - std::error_code wallet::login() + expect wallet::login_is_new() { // Remember that this function provides the strong exception guarantee. + bool new_account = false; boost::unique_lock lock{sync}; { passed_login = false; @@ -929,8 +938,9 @@ namespace lwsf { namespace internal { namespace backend if (no_subaddresses(epee::to_span(primary.subaccounts), primary.lookahead)) { lookahead_error = {}; - return {}; + return response->new_address; } + new_account = response->new_address; } // Converting `rpc::invoke` into exceptions is easier here @@ -942,7 +952,7 @@ namespace lwsf { namespace internal { namespace backend const auto response = rpc::invoke(client, login).value(); lock.lock(); if (update_lookaheads(*this, response.all_subaddrs)) - return {}; + return new_account; } // lookaheads not far enough @@ -962,7 +972,7 @@ namespace lwsf { namespace internal { namespace backend lookahead_error = handle_lookahead_error(e.code()); } - return {}; + return new_account; } std::error_code wallet::refresh(const bool mandatory) diff --git a/src/backend.h b/src/backend.h index f93fb15..7965cce 100644 --- a/src/backend.h +++ b/src/backend.h @@ -204,8 +204,15 @@ namespace lwsf { namespace internal { namespace backend { account() = delete; + struct polyseed + { + epee::byte_slice seed; + std::string passphrase; + }; + std::string address; //!< not serialized, recovered on read_bytes std::string language; + boost::optional poly; std::vector addressbook; std::vector subaccounts; //! Enabled major subaccounts. Index indicates `sub.major` value. std::unordered_map> txes; @@ -220,6 +227,7 @@ namespace lwsf { namespace internal { namespace backend bool generated_locally; //!< True iff wallet was generated and not recovered }; WIRE_DECLARE_OBJECT(account); + WIRE_DECLARE_OBJECT(account::polyseed); //! All functions should provide the strong-exception guarantee struct wallet @@ -260,10 +268,16 @@ namespace lwsf { namespace internal { namespace backend //! De-serialize `this` from msgpack. Locks+replaces contents. std::error_code from_bytes(epee::byte_slice source); + /*! Attempt login and sync subaddresses. The result of subaddress syncing, + including errors, is stored in `server_lookahead`. + \return No errors if login succeeded, and true if new account */ + expect login_is_new(); + /*! Attempt login and sync subaddresses. The result of subaddress syncing, including errors, is stored in `server_lookahead`. \return No errors if login succeeded. */ - std::error_code login(); + std::error_code login() { return login_is_new().error(); } + //! Refreshes txes information. Strong exception guarantee. std::error_code refresh(bool mandatory = false); diff --git a/src/rpc.cpp b/src/rpc.cpp index a07f3e7..d70d2ff 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -133,7 +133,7 @@ namespace lwsf { namespace internal { namespace rpc void read_bytes(wire::json_reader& source, login_response& self) { - wire::object(source, WIRE_OPTIONAL_FIELD(start_height)); + wire::object(source, WIRE_OPTIONAL_FIELD(start_height), WIRE_FIELD(new_address)); } void read_bytes(wire::json_reader& source, daemon_status& self) diff --git a/src/rpc.h b/src/rpc.h index b37c867..1d6f9b6 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -121,6 +121,7 @@ namespace lwsf { namespace internal { namespace rpc static constexpr const char* endpoint() noexcept { return "/login"; } boost::optional start_height; + bool new_address; }; void read_bytes(wire::json_reader&, login_response&); @@ -332,7 +333,7 @@ namespace lwsf { namespace internal { namespace rpc { uint64_string amount; uint64_string global_index; - std::optional recipient; + boost::optional recipient; std::uint16_t index; ringct rct; crypto::hash tx_hash; diff --git a/src/wallet.cpp b/src/wallet.cpp index f123654..9386bc0 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -38,10 +38,12 @@ #include #include #include +#include #include #include #include "address_book.h" #include "backend.h" +#include "common/expect.h" // monero/src #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "cryptonote_config.h" // monero/src #include "cryptonote_core/cryptonote_tx_utils.h" // monero/src @@ -402,6 +404,29 @@ namespace lwsf { namespace internal return out; } + wallet::wallet(error, std::string msg) + : data_(std::make_shared()), + addressbook_(), + history_(), + subaddresses_(), + subaddress_minor_(), + filename_(), + password_(), + work_queue_(), + exception_error_(std::move(msg)), + error_(), + thread_(), + iterations_(1), + mixin_(config::mixin_default), + refresh_interval_(config::refresh_interval), + refresh_notify_(), + error_sync_(), + refresh_sync_(), + thread_sync_(), + thread_state_(state::stop), + mandatory_refresh_(false) + {} + wallet::wallet(create, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds) : data_(std::make_shared()), addressbook_(), @@ -514,8 +539,7 @@ namespace lwsf { namespace internal init_wallet(*data_, recovery, nettype, false); } - - wallet::wallet(from_keys, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const std::string& address_string, const std::string& view_key, const std::string& spend_key) + wallet::wallet(from_keys, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const boost::optional& view_key, const boost::optional& spend_key) : data_(std::make_shared()), addressbook_(), history_(), @@ -540,32 +564,50 @@ namespace lwsf { namespace internal if (sodium_init() < 0) throw std::runtime_error{"Failed to initialize libsodium"}; - data_->primary.generated_locally = false; - data_->primary.type = nettype; - - if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(unwrap(unwrap(data_->primary.view.sec))), view_key)) + if (!view_key) { - exception_error_ = "view_key contained invalid hex"; + exception_error_ = "view_key is invalid"; return; } + + if (!spend_key) + { + exception_error_ = "spend_key is invalid"; + return; + } + + data_->primary.generated_locally = false; + data_->primary.type = nettype; + data_->primary.view.sec = *view_key; + data_->primary.spend.sec = *spend_key; + if (!crypto::secret_key_to_public_key(data_->primary.view.sec, data_->primary.view.pub)) { exception_error_ = "view_pub could not be computed"; return; } - crypto::secret_key spend_sec; - if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(unwrap(unwrap(data_->primary.spend.sec))), spend_key)) - { - exception_error_ = "spend_key contained invalid hex"; - return; - } if (!crypto::secret_key_to_public_key(data_->primary.spend.sec, data_->primary.spend.pub)) { exception_error_ = "spend_pub could not be computed"; return; } + } + namespace + { + boost::optional to_secret_key(const std::string& hex) + { + crypto::secret_key out{}; + if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(unwrap(unwrap(out))), hex)) + return boost::none; + return out; + } + } + + wallet::wallet(from_keys, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const std::string& address_string, const std::string& view_key, const std::string& spend_key) + : wallet(from_keys{}, nettype, std::move(filename), std::move(password), kdf_rounds, to_secret_key(view_key), to_secret_key(spend_key)) + { if (data_->get_spend_address({0, 0}) != address_string) exception_error_ = "view_key, spend_key, and address_string do not match"; } @@ -911,6 +953,58 @@ namespace lwsf { namespace internal return data_->blockchain_height == data_->primary.scan_height; } +#ifdef LWSF_POLYSEED_ENABLE + void wallet::setPolyseed(epee::byte_slice seed, std::string passphrase) + { + const boost::lock_guard lock{data_->sync}; + data_->primary.poly = backend::account::polyseed{std::move(seed), std::move(passphrase)}; + } + + bool wallet::getPolyseed(std::string &seed, std::string &passphrase) const + { + struct release_polyseed + { + void operator()(polyseed_data* ptr) const noexcept + { + if (ptr) polyseed_free(ptr); + } + }; + + std::unique_ptr cleanup{}; + + seed.clear(); + passphrase.clear(); + + const boost::lock_guard lock{data_->sync}; + if (!data_->primary.poly) + return false; + if (data_->primary.poly->seed.size() != sizeof(polyseed_storage)) + return false; + + polyseed_data* temp = nullptr; + if (polyseed_load(data_->primary.poly->seed.data(), &temp) != POLYSEED_OK) + return false; + cleanup.reset(temp); + + const polyseed_lang* lang = nullptr; + const int langs = polyseed_get_num_langs(); + for (int i = 0; i < langs; ++i) + { + lang = polyseed_get_lang(i); + if (lang && polyseed_get_lang_name(lang) == data_->primary.language) + break; + } + + if (!lang) + return false; + + seed.resize(POLYSEED_STR_SIZE); + seed.resize(polyseed_encode(temp, lang, POLYSEED_MONERO, &seed[0])); + passphrase = data_->primary.poly->passphrase; + return true; + } +#endif // LWSF_POLYSEED_ENABLE + void wallet::startRefresh() { const boost::lock_guard lock{thread_sync_}; @@ -1728,7 +1822,7 @@ namespace lwsf { namespace internal uint32_t subaddr_account, std::set subaddr_indices) { - return createTransactionMultDest({dst_addr}, payment_id, amount ? Monero::optional>{{*amount}} : std::nullopt, mixin_count, priority, subaddr_account, subaddr_indices); + return createTransactionMultDest({dst_addr}, payment_id, amount ? Monero::optional>{{*amount}} : Monero::optional>{}, mixin_count, priority, subaddr_account, subaddr_indices); } bool wallet::submitTransaction(const std::string &fileName) @@ -1887,39 +1981,41 @@ namespace lwsf { namespace internal return {}; } - static constexpr const auto hex_size = sizeof(crypto::secret_key) * 2; - - std::string out; - out.reserve(iter->second->spends.size() * hex_size); - + std::stringstream out; for (const auto& transfer : iter->second->transfers) - { - out.insert(out.end(), hex_size, 0); - if (!epee::to_hex::buffer({out.data() + out.size() - hex_size, hex_size}, epee::as_byte_span(unwrap(unwrap(transfer.secret))))) - { - set_critical(std::runtime_error{"getTxKey conversion to hex failure"}); - return {}; - } - } + epee::to_hex::buffer(out, epee::as_byte_span(unwrap(unwrap(transfer.secret)))); - return out; + return out.str(); } #ifndef LWSF_MASTER_ENABLE - bool wallet::lightWalletLogin(bool &isNewWallet) const override + bool wallet::lightWalletLogin(bool &isNewWallet) const { + isNewWallet = false; + { + const boost::lock_guard lock{data_->sync}; + if (data_->passed_login) + return true; + } + + const expect is_new = data_->login_is_new(); + if (!is_new) + return false; + isNewWallet = *is_new; + return true; } - bool wallet::lightWalletImportWalletRequest(std::string &payment_id, uint64_t &fee, bool &new_request, bool &request_fulfilled, std::string &payment_address, std::string &status) override + bool wallet::lightWalletImportWalletRequest(std::string &payment_id, uint64_t &fee, bool &new_request, bool &request_fulfilled, std::string &payment_address, std::string &status) { - expect import = data_->restore_height(0); + expect import = data_->restore_height(0); if (!import) return false; fee = std::uint64_t(import->import_fee.value_or(rpc::uint64_string(0))); new_request = import->new_request; request_fulfilled = import->request_fulfilled; - payment_address = import->payment_address.value_or({}); + payment_address = import->payment_address.value_or(std::string{}); status = std::move(import->status); + return true; } #endif // LWSF_MASTER_ENABLE diff --git a/src/wallet.h b/src/wallet.h index ec86f2f..c29aced 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -40,8 +40,9 @@ #include #include #include -#include "net/http_client.h" // monero/contrib/epee/include -#include "wallet/api/wallet2_api.h" // monero/src +#include "crypto/crypto.h" // monero/src +#include "net/http_client.h" // monero/contrib/epee/include +#include "wallet/api/wallet2_api.h" // monero/src namespace lwsf { @@ -86,6 +87,7 @@ namespace internal public: struct create{}; + struct error{}; struct from_keys{}; struct from_mnemonic{}; struct open{}; @@ -94,9 +96,11 @@ namespace internal static bool verify_password(std::string path, const std::string& password); static std::vector find(const std::string& path); + wallet(error, std::string msg); wallet(create, Monero::NetworkType nettype, std::string filename, std::string password, std::uint64_t kdf_rounds); wallet(open, Monero::NetworkType nettpe, std::string filename, std::string password, std::uint64_t kdf_rounds); wallet(from_mnemonic, Monero::NetworkType nettype, std::string filename, std::string password, std::uint64_t kdf_rounds, const std::string& mnemonic, const std::string& seed_offset); + wallet(from_keys, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const boost::optional& view_key, const boost::optional& spend_key); wallet(from_keys, Monero::NetworkType nettype, std::string filename, std::string password, std::uint64_t kdf_rounds, const std::string& address_string, const std::string& view_key, const std::string& spend_key); wallet(const wallet&) = delete; @@ -316,6 +320,11 @@ namespace internal */ virtual bool synchronized() const override; +#ifdef LWSF_POLYSEED_ENABLE + void setPolyseed(epee::byte_slice seed, std::string passphrase); + virtual bool getPolyseed(std::string &seed, std::string &passphrase) const override; +#endif + /** * @brief StartRefresh - Start/resume refresh thread (refresh every 10 seconds) */ diff --git a/src/wallet_manager.cpp b/src/wallet_manager.cpp index 6c14bff..a828b8c 100644 --- a/src/wallet_manager.cpp +++ b/src/wallet_manager.cpp @@ -39,6 +39,9 @@ #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "lwsf_config.h" #include "lws_frontend.h" +#ifdef LWSF_POLYSEED_ENABLE + #include "polyseed.h" +#endif #include "net/http_client.h" // monero/contrib/epee/include #include "net/parse.h" // monero/src #include "net/socks_connect.h" // monero/src @@ -232,8 +235,88 @@ namespace lwsf Monero::WalletListener * listener) override { //! \TODO Complete + return nullptr; } +#ifdef LWSF_POLYSEED_ENABLE + /*! + * \brief creates a wallet from a polyseed mnemonic phrase + * \param path Name of the wallet file to be created + * \param password Password of wallet file + * \param nettype Network type + * \param mnemonic Polyseed mnemonic + * \param passphrase Optional seed offset passphrase + * \param newWallet Whether it is a new wallet + * \param restoreHeight Override the embedded restore height + * \param kdf_rounds Number of rounds for key derivation function + * @return + */ + virtual Monero::Wallet * createWalletFromPolyseed(const std::string &path, + const std::string &password, + const Monero::NetworkType nettype, + const std::string &mnemonic, + const std::string &passphrase = "", + const bool newWallet = true, + const uint64_t restore_height = 0, + const uint64_t kdf_rounds = 1) override + { + struct release_polyseed + { + void operator()(polyseed_data* ptr) const noexcept + { + if (ptr) polyseed_free(ptr); + } + }; + + std::uint64_t birthday{}; + crypto::secret_key base{}; + std::string language{}; + epee::byte_slice raw_seed{}; + { + // polyseed is initialized elsewhere, a requirement of enabling polyseed in lwsf + std::unique_ptr cleanup; + + polyseed_data* temp = nullptr; + const polyseed_lang* lang = nullptr; + if (polyseed_decode(mnemonic.c_str(), POLYSEED_MONERO, &lang, &temp) != POLYSEED_OK) + return std::make_unique(wallet::error{}, "Failed polyseed decode").release(); + cleanup.reset(temp); + + language = polyseed_get_lang_name(lang); + + if (polyseed_is_encrypted(temp)) + polyseed_crypt(temp, passphrase.c_str()); + + birthday = polyseed_get_birthday(temp); + polyseed_keygen(temp, POLYSEED_MONERO, sizeof(base), reinterpret_cast(unwrap(unwrap(base)).data)); + + polyseed_storage storage{}; + polyseed_store(temp, storage); + try { raw_seed = epee::byte_slice{{storage, sizeof(storage)}}; } catch (...) {} + memwipe(storage, sizeof(storage)); + if (raw_seed.empty()) + throw std::bad_alloc{}; + } // cleanup polyseed + + if (!passphrase.empty()) + base = cryptonote::decrypt_key(base, passphrase); + + cryptonote::account_base keys{}; + keys.generate(base, true, false); + auto out = std::make_unique( + wallet::from_keys{}, nettype, path, password, kdf_rounds, keys.get_keys().m_view_secret_key, keys.get_keys().m_spend_secret_key + ); + out->setSeedLanguage(language); + out->setPolyseed(std::move(raw_seed), passphrase); + if (!newWallet) + { + //! TODO convert birthday to block height + out->setRefreshFromBlockHeight(restore_height); + } + return out.release(); + } +#endif // LWSF_POLYSEED_ENABLE + /*! * \brief Closes wallet. In case operation succeeded, wallet object deleted. in case operation failed, wallet object not deleted * \param wallet previously opened / created wallet instance @@ -394,12 +477,12 @@ namespace lwsf return cryptonote::print_money(amount); } - std::optional amountFromString(const std::string &amount) + Monero::optional amountFromString(const std::string &amount) { std::uint64_t result = 0; if (cryptonote::parse_amount(result, amount)) return result; - return std::nullopt; + return {}; } bool addressValid(const std::string &str, Monero::NetworkType nettype)