diff options
author | Riccardo Spagni <ric@spagni.net> | 2018-12-20 12:33:59 +0200 |
---|---|---|
committer | Riccardo Spagni <ric@spagni.net> | 2018-12-20 12:33:59 +0200 |
commit | c8fc06c7b6d78c83c50f96a0084f670c5670ef8b (patch) | |
tree | cb200f15b8038462a38c0051c34408d65de36f00 | |
parent | Merge pull request #4927 (diff) | |
parent | MMS (Multisig Messaging System): Initial version (diff) | |
download | monero-c8fc06c7b6d78c83c50f96a0084f670c5670ef8b.tar.xz |
Merge pull request #4849
1ebcd7b9 MMS (Multisig Messaging System): Initial version (rbrunner7)
-rw-r--r-- | src/common/util.cpp | 12 | ||||
-rw-r--r-- | src/common/util.h | 2 | ||||
-rw-r--r-- | src/simplewallet/simplewallet.cpp | 1485 | ||||
-rw-r--r-- | src/simplewallet/simplewallet.h | 45 | ||||
-rw-r--r-- | src/wallet/CMakeLists.txt | 9 | ||||
-rw-r--r-- | src/wallet/message_store.cpp | 1445 | ||||
-rw-r--r-- | src/wallet/message_store.h | 420 | ||||
-rw-r--r-- | src/wallet/message_transporter.cpp | 317 | ||||
-rw-r--r-- | src/wallet/message_transporter.h | 113 | ||||
-rw-r--r-- | src/wallet/wallet2.cpp | 117 | ||||
-rw-r--r-- | src/wallet/wallet2.h | 14 | ||||
-rw-r--r-- | src/wallet/wallet_errors.h | 25 |
12 files changed, 3877 insertions, 127 deletions
diff --git a/src/common/util.cpp b/src/common/util.cpp index 58b0d8210..448f792ff 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -58,6 +58,7 @@ #include "include_base_utils.h" #include "file_io_utils.h" #include "wipeable_string.h" +#include "misc_os_dependent.h" using namespace epee; #include "crypto/crypto.h" @@ -1025,4 +1026,15 @@ std::string get_nix_version_display_string() #endif } + std::string get_human_readable_timestamp(uint64_t ts) + { + char buffer[64]; + if (ts < 1234567890) + return "<unknown>"; + time_t tt = ts; + struct tm tm; + misc_utils::get_gmt_time(tt, tm); + strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); + return std::string(buffer); + } } diff --git a/src/common/util.h b/src/common/util.h index 1c5c5f4e7..d5aca15d1 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -242,4 +242,6 @@ namespace tools #endif void closefrom(int fd); + + std::string get_human_readable_timestamp(uint64_t ts); } diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index b729c4bb7..281702774 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -65,6 +65,7 @@ #include "wallet/wallet_args.h" #include "version.h" #include <stdexcept> +#include "wallet/message_store.h" #ifdef WIN32 #include <boost/locale.hpp> @@ -102,12 +103,14 @@ typedef cryptonote::simple_wallet sw; m_auto_refresh_enabled.store(auto_refresh_enabled, std::memory_order_relaxed); \ }) -#define SCOPED_WALLET_UNLOCK() \ +#define SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(code) \ LOCK_IDLE_SCOPE(); \ boost::optional<tools::password_container> pwd_container = boost::none; \ - if (m_wallet->ask_password() && !(pwd_container = get_and_verify_password())) { return true; } \ + if (m_wallet->ask_password() && !(pwd_container = get_and_verify_password())) { code; } \ tools::wallet_keys_unlocker unlocker(*m_wallet, pwd_container); +#define SCOPED_WALLET_UNLOCK() SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return true;) + enum TransferType { Transfer, TransferLocked, @@ -836,65 +839,83 @@ bool simple_wallet::print_fee_info(const std::vector<std::string> &args/* = std: bool simple_wallet::prepare_multisig(const std::vector<std::string> &args) { + prepare_multisig_main(args, false); + return true; +} + +bool simple_wallet::prepare_multisig_main(const std::vector<std::string> &args, bool called_by_mms) +{ if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if (m_wallet->multisig()) { fail_msg_writer() << tr("This wallet is already multisig"); - return true; + return false; } if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and cannot be made multisig"); - return true; + return false; } if(m_wallet->get_num_transfer_details()) { fail_msg_writer() << tr("This wallet has been used before, please use a new wallet to create a multisig wallet"); - return true; + return false; } - SCOPED_WALLET_UNLOCK(); + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); std::string multisig_info = m_wallet->get_multisig_info(); success_msg_writer() << multisig_info; success_msg_writer() << tr("Send this multisig info to all other participants, then use make_multisig <threshold> <info1> [<info2>...] with others' multisig info"); success_msg_writer() << tr("This includes the PRIVATE view key, so needs to be disclosed only to that multisig wallet's participants "); + + if (called_by_mms) + { + get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::key_set, multisig_info); + } + return true; } bool simple_wallet::make_multisig(const std::vector<std::string> &args) { + make_multisig_main(args, false); + return true; +} + +bool simple_wallet::make_multisig_main(const std::vector<std::string> &args, bool called_by_mms) +{ if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if (m_wallet->multisig()) { fail_msg_writer() << tr("This wallet is already multisig"); - return true; + return false; } if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and cannot be made multisig"); - return true; + return false; } if(m_wallet->get_num_transfer_details()) { fail_msg_writer() << tr("This wallet has been used before, please use a new wallet to create a multisig wallet"); - return true; + return false; } if (args.size() < 2) { fail_msg_writer() << tr("usage: make_multisig <threshold> <multisiginfo1> [<multisiginfo2>...]"); - return true; + return false; } // parse threshold @@ -902,14 +923,14 @@ bool simple_wallet::make_multisig(const std::vector<std::string> &args) if (!string_tools::get_xtype_from_string(threshold, args[0])) { fail_msg_writer() << tr("Invalid threshold"); - return true; + return false; } const auto orig_pwd_container = get_and_verify_password(); if(orig_pwd_container == boost::none) { fail_msg_writer() << tr("Your original password was incorrect."); - return true; + return false; } LOCK_IDLE_SCOPE(); @@ -924,20 +945,24 @@ bool simple_wallet::make_multisig(const std::vector<std::string> &args) success_msg_writer() << tr("Another step is needed"); success_msg_writer() << multisig_extra_info; success_msg_writer() << tr("Send this multisig info to all other participants, then use exchange_multisig_keys <info1> [<info2>...] with others' multisig info"); + if (called_by_mms) + { + get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::additional_key_set, multisig_extra_info); + } return true; } } catch (const std::exception &e) { fail_msg_writer() << tr("Error creating multisig: ") << e.what(); - return true; + return false; } uint32_t total; if (!m_wallet->multisig(NULL, &threshold, &total)) { fail_msg_writer() << tr("Error creating multisig: new wallet is not multisig"); - return true; + return false; } success_msg_writer() << std::to_string(threshold) << "/" << total << tr(" multisig address: ") << m_wallet->get_account().get_public_address_str(m_wallet->nettype()); @@ -997,35 +1022,41 @@ bool simple_wallet::finalize_multisig(const std::vector<std::string> &args) return true; } -bool simple_wallet::exchange_multisig_keys(const std::vector<std::string> &args) { +bool simple_wallet::exchange_multisig_keys(const std::vector<std::string> &args) +{ + exchange_multisig_keys_main(args, false); + return true; +} + +bool simple_wallet::exchange_multisig_keys_main(const std::vector<std::string> &args, bool called_by_mms) { bool ready; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if (!m_wallet->multisig(&ready)) { fail_msg_writer() << tr("This wallet is not multisig"); - return true; + return false; } if (ready) { fail_msg_writer() << tr("This wallet is already finalized"); - return true; + return false; } const auto orig_pwd_container = get_and_verify_password(); if(orig_pwd_container == boost::none) { fail_msg_writer() << tr("Your original password was incorrect."); - return true; + return false; } if (args.size() < 2) { fail_msg_writer() << tr("usage: exchange_multisig_keys <multisiginfo1> [<multisiginfo2>...]"); - return true; + return false; } try @@ -1036,6 +1067,10 @@ bool simple_wallet::exchange_multisig_keys(const std::vector<std::string> &args) message_writer() << tr("Another step is needed"); message_writer() << multisig_extra_info; message_writer() << tr("Send this multisig info to all other participants, then use exchange_multisig_keys <info1> [<info2>...] with others' multisig info"); + if (called_by_mms) + { + get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::additional_key_set, multisig_extra_info); + } return true; } else { uint32_t threshold, total; @@ -1046,7 +1081,7 @@ bool simple_wallet::exchange_multisig_keys(const std::vector<std::string> &args) catch (const std::exception &e) { fail_msg_writer() << tr("Failed to perform multisig keys exchange: ") << e.what(); - return true; + return false; } return true; @@ -1054,50 +1089,63 @@ bool simple_wallet::exchange_multisig_keys(const std::vector<std::string> &args) bool simple_wallet::export_multisig(const std::vector<std::string> &args) { + export_multisig_main(args, false); + return true; +} + +bool simple_wallet::export_multisig_main(const std::vector<std::string> &args, bool called_by_mms) +{ bool ready; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if (!m_wallet->multisig(&ready)) { fail_msg_writer() << tr("This wallet is not multisig"); - return true; + return false; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); - return true; + return false; } if (args.size() != 1) { fail_msg_writer() << tr("usage: export_multisig_info <filename>"); - return true; + return false; } const std::string filename = args[0]; - if (m_wallet->confirm_export_overwrite() && !check_file_overwrite(filename)) + if (!called_by_mms && m_wallet->confirm_export_overwrite() && !check_file_overwrite(filename)) return true; - SCOPED_WALLET_UNLOCK(); + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); try { cryptonote::blobdata ciphertext = m_wallet->export_multisig(); - bool r = epee::file_io_utils::save_string_to_file(filename, ciphertext); - if (!r) + if (called_by_mms) { - fail_msg_writer() << tr("failed to save file ") << filename; - return true; + get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::multisig_sync_data, ciphertext); + } + else + { + bool r = epee::file_io_utils::save_string_to_file(filename, ciphertext); + if (!r) + { + fail_msg_writer() << tr("failed to save file ") << filename; + return false; + } } } catch (const std::exception &e) { LOG_ERROR("Error exporting multisig info: " << e.what()); fail_msg_writer() << tr("Error exporting multisig info: ") << e.what(); - return true; + return false; } success_msg_writer() << tr("Multisig info exported to ") << filename; @@ -1106,44 +1154,57 @@ bool simple_wallet::export_multisig(const std::vector<std::string> &args) bool simple_wallet::import_multisig(const std::vector<std::string> &args) { + import_multisig_main(args, false); + return true; +} + +bool simple_wallet::import_multisig_main(const std::vector<std::string> &args, bool called_by_mms) +{ bool ready; uint32_t threshold, total; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if (!m_wallet->multisig(&ready, &threshold, &total)) { fail_msg_writer() << tr("This wallet is not multisig"); - return true; + return false; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); - return true; + return false; } if (args.size() < threshold - 1) { fail_msg_writer() << tr("usage: import_multisig_info <filename1> [<filename2>...] - one for each other participant"); - return true; + return false; } std::vector<cryptonote::blobdata> info; for (size_t n = 0; n < args.size(); ++n) { - const std::string filename = args[n]; - std::string data; - bool r = epee::file_io_utils::load_file_to_string(filename, data); - if (!r) + if (called_by_mms) { - fail_msg_writer() << tr("failed to read file ") << filename; - return true; + info.push_back(args[n]); + } + else + { + const std::string &filename = args[n]; + std::string data; + bool r = epee::file_io_utils::load_file_to_string(filename, data); + if (!r) + { + fail_msg_writer() << tr("failed to read file ") << filename; + return false; + } + info.push_back(std::move(data)); } - info.push_back(std::move(data)); } - SCOPED_WALLET_UNLOCK(); + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); // all read and parsed, actually import try @@ -1158,7 +1219,7 @@ bool simple_wallet::import_multisig(const std::vector<std::string> &args) catch (const std::exception &e) { fail_msg_writer() << tr("Failed to import multisig info: ") << e.what(); - return true; + return false; } if (m_wallet->is_trusted_daemon()) { @@ -1169,11 +1230,13 @@ bool simple_wallet::import_multisig(const std::vector<std::string> &args) catch (const std::exception &e) { message_writer() << tr("Failed to update spent status after importing multisig info: ") << e.what(); + return false; } } else { message_writer() << tr("Untrusted daemon, spent status may be incorrect. Use a trusted daemon and run \"rescan_spent\""); + return false; } return true; } @@ -1186,51 +1249,93 @@ bool simple_wallet::accept_loaded_tx(const tools::wallet2::multisig_tx_set &txs) bool simple_wallet::sign_multisig(const std::vector<std::string> &args) { + sign_multisig_main(args, false); + return true; +} + +bool simple_wallet::sign_multisig_main(const std::vector<std::string> &args, bool called_by_mms) +{ bool ready; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if(!m_wallet->multisig(&ready)) { fail_msg_writer() << tr("This is not a multisig wallet"); - return true; + return false; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); - return true; + return false; } if (args.size() != 1) { fail_msg_writer() << tr("usage: sign_multisig <filename>"); - return true; + return false; } - SCOPED_WALLET_UNLOCK(); + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); std::string filename = args[0]; std::vector<crypto::hash> txids; uint32_t signers = 0; try { - bool r = m_wallet->sign_multisig_tx_from_file(filename, txids, [&](const tools::wallet2::multisig_tx_set &tx){ signers = tx.m_signers.size(); return accept_loaded_tx(tx); }); - if (!r) + if (called_by_mms) { - fail_msg_writer() << tr("Failed to sign multisig transaction"); - return true; + tools::wallet2::multisig_tx_set exported_txs; + std::string ciphertext; + bool r = m_wallet->load_multisig_tx(args[0], exported_txs, [&](const tools::wallet2::multisig_tx_set &tx){ signers = tx.m_signers.size(); return accept_loaded_tx(tx); }); + if (r) + { + r = m_wallet->sign_multisig_tx(exported_txs, txids); + } + if (r) + { + ciphertext = m_wallet->save_multisig_tx(exported_txs); + if (ciphertext.empty()) + { + r = false; + } + } + if (r) + { + mms::message_type message_type = mms::message_type::fully_signed_tx; + if (txids.empty()) + { + message_type = mms::message_type::partially_signed_tx; + } + get_message_store().process_wallet_created_data(get_multisig_wallet_state(), message_type, ciphertext); + filename = "MMS"; // for the messages below + } + else + { + fail_msg_writer() << tr("Failed to sign multisig transaction"); + return false; + } + } + else + { + bool r = m_wallet->sign_multisig_tx_from_file(filename, txids, [&](const tools::wallet2::multisig_tx_set &tx){ signers = tx.m_signers.size(); return accept_loaded_tx(tx); }); + if (!r) + { + fail_msg_writer() << tr("Failed to sign multisig transaction"); + return false; + } } } catch (const tools::error::multisig_export_needed& e) { fail_msg_writer() << tr("Multisig error: ") << e.what(); - return true; + return false; } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to sign multisig transaction: ") << e.what(); - return true; + return false; } if (txids.empty()) @@ -1259,49 +1364,67 @@ bool simple_wallet::sign_multisig(const std::vector<std::string> &args) bool simple_wallet::submit_multisig(const std::vector<std::string> &args) { + submit_multisig_main(args, false); + return true; +} + +bool simple_wallet::submit_multisig_main(const std::vector<std::string> &args, bool called_by_mms) +{ bool ready; uint32_t threshold; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); - return true; + return false; } if (!m_wallet->multisig(&ready, &threshold)) { fail_msg_writer() << tr("This is not a multisig wallet"); - return true; + return false; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); - return true; + return false; } if (args.size() != 1) { fail_msg_writer() << tr("usage: submit_multisig <filename>"); - return true; + return false; } if (!try_connect_to_daemon()) - return true; + return false; - SCOPED_WALLET_UNLOCK(); + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); std::string filename = args[0]; try { tools::wallet2::multisig_tx_set txs; - bool r = m_wallet->load_multisig_tx_from_file(filename, txs, [&](const tools::wallet2::multisig_tx_set &tx){ return accept_loaded_tx(tx); }); - if (!r) + if (called_by_mms) { - fail_msg_writer() << tr("Failed to load multisig transaction from file"); - return true; + bool r = m_wallet->load_multisig_tx(args[0], txs, [&](const tools::wallet2::multisig_tx_set &tx){ return accept_loaded_tx(tx); }); + if (!r) + { + fail_msg_writer() << tr("Failed to load multisig transaction from MMS"); + return false; + } + } + else + { + bool r = m_wallet->load_multisig_tx_from_file(filename, txs, [&](const tools::wallet2::multisig_tx_set &tx){ return accept_loaded_tx(tx); }); + if (!r) + { + fail_msg_writer() << tr("Failed to load multisig transaction from file"); + return false; + } } if (txs.m_signers.size() < threshold) { fail_msg_writer() << (boost::format(tr("Multisig transaction signed by only %u signers, needs %u more signatures")) % txs.m_signers.size() % (threshold - txs.m_signers.size())).str(); - return true; + return false; } // actually commit the transactions @@ -1320,6 +1443,7 @@ bool simple_wallet::submit_multisig(const std::vector<std::string> &args) { LOG_ERROR("unknown error"); fail_msg_writer() << tr("unknown error"); + return false; } return true; @@ -2308,6 +2432,12 @@ bool simple_wallet::help(const std::vector<std::string> &args/* = std::vector<st { success_msg_writer() << get_commands_str(); } + else if ((args.size() == 2) && (args.front() == "mms")) + { + // Little hack to be able to do "help mms <subcommand>" + std::vector<std::string> mms_args(1, args.front() + " " + args.back()); + success_msg_writer() << get_command_usage(mms_args); + } else { success_msg_writer() << get_command_usage(args); @@ -2655,6 +2785,89 @@ simple_wallet::simple_wallet() boost::bind(&simple_wallet::export_raw_multisig, this, _1), tr("export_raw_multisig_tx <filename>"), tr("Export a signed multisig transaction to a file")); + m_cmd_binder.set_handler("mms", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms [<subcommand> [<subcommand_parameters>]]"), + tr("Interface with the MMS (Multisig Messaging System)\n" + "<subcommand> is one of:\n" + " init, info, signer, list, next, sync, transfer, delete, send, receive, export, note, show, set, help\n" + " send_signer_config, start_auto_config, stop_auto_config, auto_config\n" + "Get help about a subcommand with: help mms <subcommand>, or mms help <subcommand>")); + m_cmd_binder.set_handler("mms init", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms init <required_signers>/<authorized_signers> <own_label> <own_transport_address>"), + tr("Initialize and configure the MMS for M/N = number of required signers/number of authorized signers multisig")); + m_cmd_binder.set_handler("mms info", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms info"), + tr("Display current MMS configuration")); + m_cmd_binder.set_handler("mms signer", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms signer [<number> <label> [<transport_address> [<monero_address>]]]"), + tr("Set or modify authorized signer info (single-word label, transport address, Monero address), or list all signers")); + m_cmd_binder.set_handler("mms list", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms list"), + tr("List all messages")); + m_cmd_binder.set_handler("mms next", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms next [sync]"), + tr("Evaluate the next possible multisig-related action(s) according to wallet state, and execute or offer for choice\n" + "By using 'sync' processing of waiting messages with multisig sync info can be forced regardless of wallet state")); + m_cmd_binder.set_handler("mms sync", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms sync"), + tr("Force generation of multisig sync info regardless of wallet state, to recover from special situations like \"stale data\" errors")); + m_cmd_binder.set_handler("mms transfer", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms transfer <transfer_command_arguments>"), + tr("Initiate transfer with MMS support; arguments identical to normal 'transfer' command arguments, for info see there")); + m_cmd_binder.set_handler("mms delete", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms delete (<message_id> | all)"), + tr("Delete a single message by giving its id, or delete all messages by using 'all'")); + m_cmd_binder.set_handler("mms send", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms send [<message_id>]"), + tr("Send a single message by giving its id, or send all waiting messages")); + m_cmd_binder.set_handler("mms receive", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms receive"), + tr("Check right away for new messages to receive")); + m_cmd_binder.set_handler("mms export", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms export <message_id"), + tr("Write the content of a message to a file \"mms_message_content\"")); + m_cmd_binder.set_handler("mms note", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms note [<label> <text>]"), + tr("Send a one-line message to an authorized signer, identified by its label, or show any waiting unread notes")); + m_cmd_binder.set_handler("mms show", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms show <message_id>"), + tr("Show detailed info about a single message")); + m_cmd_binder.set_handler("mms set", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms set <option_name> [<option_value>]"), + tr("Available options:\n " + "auto-send <1|0>\n " + " Whether to automatically send newly generated messages right away.\n ")); + m_cmd_binder.set_handler("mms send_message_config", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms send_signer_config"), + tr("Send completed signer config to all other authorized signers")); + m_cmd_binder.set_handler("mms start_auto_config", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms start_auto_config [<label> <label> ...]"), + tr("Start auto-config at the auto-config manager's wallet by issuing auto-config tokens and optionally set others' labels")); + m_cmd_binder.set_handler("mms stop_auto_config", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms stop_auto_config"), + tr("Delete any auto-config tokens and abort a auto-config process")); + m_cmd_binder.set_handler("mms auto_config", + boost::bind(&simple_wallet::mms, this, _1), + tr("mms auto_config <auto_config_token>"), + tr("Start auto-config by using the token received from the auto-config manager")); m_cmd_binder.set_handler("print_ring", boost::bind(&simple_wallet::print_ring, this, _1), tr("print_ring <key_image> | <txid>"), @@ -4843,11 +5056,11 @@ bool simple_wallet::print_ring_members(const std::vector<tools::wallet2::pending return true; } //---------------------------------------------------------------------------------------------------- -bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::string> &args_) +bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::string> &args_, bool called_by_mms) { // "transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <address> <amount> [<payment_id>]" if (!try_connect_to_daemon()) - return true; + return false; std::vector<std::string> local_args = args_; @@ -4855,7 +5068,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (local_args.size() > 0 && local_args[0].substr(0, 6) == "index=") { if (!parse_subaddress_indices(local_args[0], subaddr_indices)) - return true; + return false; local_args.erase(local_args.begin()); } @@ -4877,7 +5090,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri else if (ring_size == 0) { fail_msg_writer() << tr("Ring size must not be 0"); - return true; + return false; } else { @@ -4889,19 +5102,19 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (adjusted_fake_outs_count > fake_outs_count) { fail_msg_writer() << (boost::format(tr("ring size %u is too small, minimum is %u")) % (fake_outs_count+1) % (adjusted_fake_outs_count+1)).str(); - return true; + return false; } if (adjusted_fake_outs_count < fake_outs_count) { fail_msg_writer() << (boost::format(tr("ring size %u is too large, maximum is %u")) % (fake_outs_count+1) % (adjusted_fake_outs_count+1)).str(); - return true; + return false; } const size_t min_args = (transfer_type == TransferLocked) ? 2 : 1; if(local_args.size() < min_args) { fail_msg_writer() << tr("wrong number of arguments"); - return true; + return false; } std::vector<uint8_t> extra; @@ -4936,7 +5149,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if(!r) { fail_msg_writer() << tr("payment id failed to encode"); - return true; + return false; } } @@ -4950,12 +5163,12 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri catch (const std::exception &e) { fail_msg_writer() << tr("bad locked_blocks parameter:") << " " << local_args.back(); - return true; + return false; } if (locked_blocks > 1000000) { fail_msg_writer() << tr("Locked blocks too high, max 1000000 (˜4 yrs)"); - return true; + return false; } local_args.pop_back(); } @@ -4983,7 +5196,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (!tools::wallet2::parse_short_payment_id(payment_id_uri, info.payment_id)) { fail_msg_writer() << tr("failed to parse short payment ID from URI"); - return true; + return false; } info.has_payment_id = true; } @@ -4998,7 +5211,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri { fail_msg_writer() << tr("amount is wrong: ") << local_args[i] << ' ' << local_args[i + 1] << ", " << tr("expected number from 0 to ") << print_money(std::numeric_limits<uint64_t>::max()); - return true; + return false; } i += 2; } @@ -5008,13 +5221,13 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri fail_msg_writer() << tr("Invalid last argument: ") << local_args.back() << ": " << error; else fail_msg_writer() << tr("Invalid last argument: ") << local_args.back(); - return true; + return false; } if (!r) { fail_msg_writer() << tr("failed to parse address"); - return true; + return false; } de.addr = info.address; de.is_subaddress = info.is_subaddress; @@ -5025,7 +5238,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (payment_id_seen) { fail_msg_writer() << tr("a single transaction cannot use more than one payment id"); - return true; + return false; } crypto::hash payment_id; @@ -5042,13 +5255,13 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri else { fail_msg_writer() << tr("failed to parse payment id, though it was detected"); - return true; + return false; } bool r = add_extra_nonce_to_tx_extra(extra, extra_nonce); if(!r) { fail_msg_writer() << tr("failed to set up payment id, though it was decoded correctly"); - return true; + return false; } payment_id_seen = true; } @@ -5061,16 +5274,16 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri { std::string accepted = input_line(tr("No payment id is included with this transaction. Is this okay? (Y/Yes/N/No): ")); if (std::cin.eof()) - return true; + return false; if (!command_line::is_yes(accepted)) { fail_msg_writer() << tr("transaction cancelled."); - return true; + return false; } } - SCOPED_WALLET_UNLOCK(); + SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); try { @@ -5085,7 +5298,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (!err.empty()) { fail_msg_writer() << tr("failed to get blockchain height: ") << err; - return true; + return false; } unlock_block = bc_height + locked_blocks; ptx_vector = m_wallet->create_transactions_2(dsts, fake_outs_count, unlock_block /* unlock_time */, priority, extra, m_current_subaddress_account, subaddr_indices); @@ -5101,7 +5314,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (ptx_vector.empty()) { fail_msg_writer() << tr("No outputs found, or daemon is not ready"); - return true; + return false; } // if we need to check for backlog, check the worst case tx @@ -5141,12 +5354,12 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri { std::string accepted = input_line(prompt_str); if (std::cin.eof()) - return true; + return false; if (!command_line::is_yes(accepted)) { fail_msg_writer() << tr("transaction cancelled."); - return true; + return false; } } } @@ -5206,7 +5419,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (m_wallet->print_ring_members()) { if (!print_ring_members(ptx_vector, prompt)) - return true; + return false; } bool default_ring_size = true; for (const auto &ptx: ptx_vector) @@ -5229,22 +5442,32 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri std::string accepted = input_line(prompt.str()); if (std::cin.eof()) - return true; + return false; if (!command_line::is_yes(accepted)) { fail_msg_writer() << tr("transaction cancelled."); - return true; + return false; } } // actually commit the transactions - if (m_wallet->multisig()) + if (m_wallet->multisig() && called_by_mms) + { + std::string ciphertext = m_wallet->save_multisig_tx(ptx_vector); + if (!ciphertext.empty()) + { + get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::partially_signed_tx, ciphertext); + success_msg_writer(true) << tr("Unsigned transaction(s) successfully written to MMS"); + } + } + else if (m_wallet->multisig()) { bool r = m_wallet->save_multisig_tx(ptx_vector, "multisig_monero_tx"); if (!r) { fail_msg_writer() << tr("Failed to write transaction(s) to file"); + return false; } else { @@ -5258,7 +5481,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri tools::wallet2::signed_tx_set signed_tx; if (!cold_sign_tx(ptx_vector, signed_tx, dsts_info, [&](const tools::wallet2::signed_tx_set &tx){ return accept_loaded_tx(tx); })){ fail_msg_writer() << tr("Failed to cold sign transaction with HW wallet"); - return true; + return false; } commit_or_save(signed_tx.ptx, m_do_not_relay); @@ -5266,11 +5489,13 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri catch (const std::exception& e) { handle_transfer_exception(std::current_exception(), m_wallet->is_trusted_daemon()); + return false; } catch (...) { LOG_ERROR("Unknown error"); fail_msg_writer() << tr("unknown error"); + return false; } } else if (m_wallet->watch_only()) @@ -5279,6 +5504,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri if (!r) { fail_msg_writer() << tr("Failed to write transaction(s) to file"); + return false; } else { @@ -5293,11 +5519,13 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri catch (const std::exception &e) { handle_transfer_exception(std::current_exception(), m_wallet->is_trusted_daemon()); + return false; } catch (...) { LOG_ERROR("unknown error"); fail_msg_writer() << tr("unknown error"); + return false; } return true; @@ -5305,12 +5533,14 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri //---------------------------------------------------------------------------------------------------- bool simple_wallet::transfer(const std::vector<std::string> &args_) { - return transfer_main(Transfer, args_); + transfer_main(Transfer, args_, false); + return true; } //---------------------------------------------------------------------------------------------------- bool simple_wallet::locked_transfer(const std::vector<std::string> &args_) { - return transfer_main(TransferLocked, args_); + transfer_main(TransferLocked, args_, false); + return true; } //---------------------------------------------------------------------------------------------------- bool simple_wallet::locked_sweep_all(const std::vector<std::string> &args_) @@ -6799,19 +7029,6 @@ bool simple_wallet::check_reserve_proof(const std::vector<std::string> &args) return true; } //---------------------------------------------------------------------------------------------------- -static std::string get_human_readable_timestamp(uint64_t ts) -{ - char buffer[64]; - if (ts < 1234567890) - return "<unknown>"; - time_t tt = ts; - struct tm tm; - epee::misc_utils::get_gmt_time(tt, tm); - uint64_t now = time(NULL); - strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); - return std::string(buffer); -} -//---------------------------------------------------------------------------------------------------- static std::string get_human_readable_timespan(std::chrono::seconds seconds) { uint64_t ts = seconds.count(); @@ -7096,7 +7313,7 @@ bool simple_wallet::show_transfers(const std::vector<std::string> &args_) % transfer.block % transfer.direction % transfer.unlocked - % get_human_readable_timestamp(transfer.timestamp) + % tools::get_human_readable_timestamp(transfer.timestamp) % print_money(transfer.amount) % string_tools::pod_to_hex(transfer.hash) % transfer.payment_id @@ -7160,7 +7377,7 @@ bool simple_wallet::export_transfers(const std::vector<std::string>& args_) % transfer.block % transfer.direction % transfer.unlocked - % get_human_readable_timestamp(transfer.timestamp) + % tools::get_human_readable_timestamp(transfer.timestamp) % print_money(transfer.amount) % print_money(running_balance) % string_tools::pod_to_hex(transfer.hash) @@ -7363,6 +7580,22 @@ bool simple_wallet::rescan_blockchain(const std::vector<std::string> &args_) return refresh_main(0, hard ? ResetHard : ResetSoft, true); } //---------------------------------------------------------------------------------------------------- +void simple_wallet::check_for_messages() +{ + try + { + std::vector<mms::message> new_messages; + bool new_message = get_message_store().check_for_messages(get_multisig_wallet_state(), new_messages); + if (new_message) + { + message_writer(console_color_magenta, true) << tr("MMS received new message"); + list_mms_messages(new_messages); + m_cmd_binder.print_prompt(); + } + } + catch(...) {} +} +//---------------------------------------------------------------------------------------------------- void simple_wallet::wallet_idle_thread() { while (true) @@ -7385,6 +7618,14 @@ void simple_wallet::wallet_idle_thread() m_auto_refresh_refreshing = false; } + // Check for new MMS messages; + // For simplicity auto message check is ALSO controlled by "m_auto_refresh_enabled" and has no + // separate thread either; thread syncing is tricky enough with only this one idle thread here + if (m_auto_refresh_enabled && get_message_store().get_active()) + { + check_for_messages(); + } + if (!m_idle_run.load(std::memory_order_relaxed)) break; m_idle_cond.wait_for(lock, boost::chrono::seconds(90)); @@ -8336,7 +8577,7 @@ bool simple_wallet::show_transfer(const std::vector<std::string> &args) success_msg_writer() << "Incoming transaction found"; success_msg_writer() << "txid: " << txid; success_msg_writer() << "Height: " << pd.m_block_height; - success_msg_writer() << "Timestamp: " << get_human_readable_timestamp(pd.m_timestamp); + success_msg_writer() << "Timestamp: " << tools::get_human_readable_timestamp(pd.m_timestamp); success_msg_writer() << "Amount: " << print_money(pd.m_amount); success_msg_writer() << "Payment ID: " << payment_id; if (pd.m_unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) @@ -8386,7 +8627,7 @@ bool simple_wallet::show_transfer(const std::vector<std::string> &args) success_msg_writer() << "Outgoing transaction found"; success_msg_writer() << "txid: " << txid; success_msg_writer() << "Height: " << pd.m_block_height; - success_msg_writer() << "Timestamp: " << get_human_readable_timestamp(pd.m_timestamp); + success_msg_writer() << "Timestamp: " << tools::get_human_readable_timestamp(pd.m_timestamp); success_msg_writer() << "Amount: " << print_money(pd.m_amount_in - change - fee); success_msg_writer() << "Payment ID: " << payment_id; success_msg_writer() << "Change: " << print_money(change); @@ -8411,7 +8652,7 @@ bool simple_wallet::show_transfer(const std::vector<std::string> &args) payment_id = payment_id.substr(0,16); success_msg_writer() << "Unconfirmed incoming transaction found in the txpool"; success_msg_writer() << "txid: " << txid; - success_msg_writer() << "Timestamp: " << get_human_readable_timestamp(pd.m_timestamp); + success_msg_writer() << "Timestamp: " << tools::get_human_readable_timestamp(pd.m_timestamp); success_msg_writer() << "Amount: " << print_money(pd.m_amount); success_msg_writer() << "Payment ID: " << payment_id; success_msg_writer() << "Address index: " << pd.m_subaddr_index.minor; @@ -8442,7 +8683,7 @@ bool simple_wallet::show_transfer(const std::vector<std::string> &args) success_msg_writer() << (is_failed ? "Failed" : "Pending") << " outgoing transaction found"; success_msg_writer() << "txid: " << txid; - success_msg_writer() << "Timestamp: " << get_human_readable_timestamp(pd.m_timestamp); + success_msg_writer() << "Timestamp: " << tools::get_human_readable_timestamp(pd.m_timestamp); success_msg_writer() << "Amount: " << print_money(amount - pd.m_change - fee); success_msg_writer() << "Payment ID: " << payment_id; success_msg_writer() << "Change: " << print_money(pd.m_change); @@ -8602,3 +8843,1009 @@ int main(int argc, char* argv[]) return 0; CATCH_ENTRY_L0("main", 1); } + +// MMS --------------------------------------------------------------------------------------------------- + +// Access to the message store, or more exactly to the list of the messages that can be changed +// by the idle thread, is guarded by the same mutex-based mechanism as access to the wallet +// as a whole and thus e.g. uses the "LOCK_IDLE_SCOPE" macro. This is a little over-cautious, but +// simple and safe. Care has to be taken however where MMS methods call other simplewallet methods +// that use "LOCK_IDLE_SCOPE" as this cannot be nested! + +// Methods for commands like "export_multisig_info" usually read data from file(s) or write data +// to files. The MMS calls now those methods as well, to produce data for messages and to process data +// from messages. As it would be quite inconvenient for the MMS to write data for such methods to files +// first or get data out of result files after the call, those methods detect a call from the MMS and +// expect data as arguments instead of files and give back data by calling 'process_wallet_created_data'. + +bool simple_wallet::user_confirms(const std::string &question) +{ + std::string answer = input_line(question + tr(" (Y/Yes/N/No): ")); + return !std::cin.eof() && command_line::is_yes(answer); +} + +bool simple_wallet::get_number_from_arg(const std::string &arg, uint32_t &number, const uint32_t lower_bound, const uint32_t upper_bound) +{ + bool valid = false; + try + { + number = boost::lexical_cast<uint32_t>(arg); + valid = (number >= lower_bound) && (number <= upper_bound); + } + catch(const boost::bad_lexical_cast &) + { + } + return valid; +} + +bool simple_wallet::choose_mms_processing(const std::vector<mms::processing_data> &data_list, uint32_t &choice) +{ + size_t choices = data_list.size(); + if (choices == 1) + { + choice = 0; + return true; + } + mms::message_store& ms = m_wallet->get_message_store(); + message_writer() << tr("Choose processing:"); + std::string text; + for (size_t i = 0; i < choices; ++i) + { + const mms::processing_data &data = data_list[i]; + text = std::to_string(i+1) + ": "; + switch (data.processing) + { + case mms::message_processing::sign_tx: + text += tr("Sign tx"); + break; + case mms::message_processing::send_tx: + { + mms::message m; + ms.get_message_by_id(data.message_ids[0], m); + if (m.type == mms::message_type::fully_signed_tx) + { + text += tr("Send the tx for submission to "); + } + else + { + text += tr("Send the tx for signing to "); + } + mms::authorized_signer signer = ms.get_signer(data.receiving_signer_index); + text += ms.signer_to_string(signer, 50); + break; + } + case mms::message_processing::submit_tx: + text += tr("Submit tx"); + break; + default: + text += tr("unknown"); + break; + } + message_writer() << text; + } + + std::string line = input_line(tr("Choice: ")); + if (std::cin.eof() || line.empty()) + { + return false; + } + bool choice_ok = get_number_from_arg(line, choice, 1, choices); + if (choice_ok) + { + choice--; + } + else + { + fail_msg_writer() << tr("Wrong choice"); + } + return choice_ok; +} + +void simple_wallet::list_mms_messages(const std::vector<mms::message> &messages) +{ + message_writer() << boost::format("%4s %-4s %-30s %-21s %7s %3s %-15s %-40s") % tr("Id") % tr("I/O") % tr("Authorized Signer") + % tr("Message Type") % tr("Height") % tr("R") % tr("Message State") % tr("Since"); + mms::message_store& ms = m_wallet->get_message_store(); + uint64_t now = (uint64_t)time(NULL); + for (size_t i = 0; i < messages.size(); ++i) + { + const mms::message &m = messages[i]; + const mms::authorized_signer &signer = ms.get_signer(m.signer_index); + bool highlight = (m.state == mms::message_state::ready_to_send) || (m.state == mms::message_state::waiting); + message_writer(m.direction == mms::message_direction::out ? console_color_green : console_color_magenta, highlight) << + boost::format("%4s %-4s %-30s %-21s %7s %3s %-15s %-40s") % + m.id % + ms.message_direction_to_string(m.direction) % + ms.signer_to_string(signer, 30) % + ms.message_type_to_string(m.type) % + m.wallet_height % + m.round % + ms.message_state_to_string(m.state) % + (tools::get_human_readable_timestamp(m.modified) + ", " + get_human_readable_timespan(std::chrono::seconds(now - m.modified)) + tr(" ago")); + } +} + +void simple_wallet::list_signers(const std::vector<mms::authorized_signer> &signers) +{ + message_writer() << boost::format("%2s %-20s %-s") % tr("#") % tr("Label") % tr("Transport Address"); + message_writer() << boost::format("%2s %-20s %-s") % "" % tr("Auto-Config Token") % tr("Monero Address"); + for (size_t i = 0; i < signers.size(); ++i) + { + const mms::authorized_signer &signer = signers[i]; + std::string label = signer.label.empty() ? tr("<not set>") : signer.label; + std::string monero_address; + if (signer.monero_address_known) + { + monero_address = get_account_address_as_str(m_wallet->nettype(), false, signer.monero_address); + } + else + { + monero_address = tr("<not set>"); + } + std::string transport_address = signer.transport_address.empty() ? tr("<not set>") : signer.transport_address; + message_writer() << boost::format("%2s %-20s %-s") % (i + 1) % label % transport_address; + message_writer() << boost::format("%2s %-20s %-s") % "" % signer.auto_config_token % monero_address; + message_writer() << ""; + } +} + +void simple_wallet::add_signer_config_messages() +{ + mms::message_store& ms = m_wallet->get_message_store(); + std::string signer_config; + ms.get_signer_config(signer_config); + + const std::vector<mms::authorized_signer> signers = ms.get_all_signers(); + mms::multisig_wallet_state state = get_multisig_wallet_state(); + uint32_t num_authorized_signers = ms.get_num_authorized_signers(); + for (uint32_t i = 1 /* without me */; i < num_authorized_signers; ++i) + { + ms.add_message(state, i, mms::message_type::signer_config, mms::message_direction::out, signer_config); + } +} + +void simple_wallet::show_message(const mms::message &m) +{ + mms::message_store& ms = m_wallet->get_message_store(); + const mms::authorized_signer &signer = ms.get_signer(m.signer_index); + bool display_content; + std::string sanitized_text; + switch (m.type) + { + case mms::message_type::key_set: + case mms::message_type::additional_key_set: + case mms::message_type::note: + display_content = true; + ms.get_sanitized_message_text(m, sanitized_text); + break; + default: + display_content = false; + } + uint64_t now = (uint64_t)time(NULL); + message_writer() << ""; + message_writer() << tr("Message ") << m.id; + message_writer() << tr("In/out: ") << ms.message_direction_to_string(m.direction); + message_writer() << tr("Type: ") << ms.message_type_to_string(m.type); + message_writer() << tr("State: ") << boost::format(tr("%s since %s, %s ago")) % + ms.message_state_to_string(m.state) % tools::get_human_readable_timestamp(m.modified) % get_human_readable_timespan(std::chrono::seconds(now - m.modified)); + if (m.sent == 0) + { + message_writer() << tr("Sent: Never"); + } + else + { + message_writer() << boost::format(tr("Sent: %s, %s ago")) % + tools::get_human_readable_timestamp(m.sent) % get_human_readable_timespan(std::chrono::seconds(now - m.sent)); + } + message_writer() << tr("Authorized signer: ") << ms.signer_to_string(signer, 100); + message_writer() << tr("Content size: ") << m.content.length() << tr(" bytes"); + message_writer() << tr("Content: ") << (display_content ? sanitized_text : tr("(binary data)")); + + if (m.type == mms::message_type::note) + { + // Showing a note and read its text is "processing" it: Set the state accordingly + // which will also delete it from Bitmessage as a side effect + // (Without this little "twist" it would never change the state, and never get deleted) + ms.set_message_processed_or_sent(m.id); + } +} + +void simple_wallet::ask_send_all_ready_messages() +{ + mms::message_store& ms = m_wallet->get_message_store(); + std::vector<mms::message> ready_messages; + const std::vector<mms::message> &messages = ms.get_all_messages(); + for (size_t i = 0; i < messages.size(); ++i) + { + const mms::message &m = messages[i]; + if (m.state == mms::message_state::ready_to_send) + { + ready_messages.push_back(m); + } + } + if (ready_messages.size() != 0) + { + list_mms_messages(ready_messages); + bool send = ms.get_auto_send(); + if (!send) + { + send = user_confirms(tr("Send these messages now?")); + } + if (send) + { + mms::multisig_wallet_state state = get_multisig_wallet_state(); + for (size_t i = 0; i < ready_messages.size(); ++i) + { + ms.send_message(state, ready_messages[i].id); + ms.set_message_processed_or_sent(ready_messages[i].id); + } + success_msg_writer() << tr("Queued for sending."); + } + } +} + +bool simple_wallet::get_message_from_arg(const std::string &arg, mms::message &m) +{ + mms::message_store& ms = m_wallet->get_message_store(); + bool valid_id = false; + uint32_t id; + try + { + id = (uint32_t)boost::lexical_cast<uint32_t>(arg); + valid_id = ms.get_message_by_id(id, m); + } + catch (const boost::bad_lexical_cast &) + { + } + if (!valid_id) + { + fail_msg_writer() << tr("Invalid message id"); + } + return valid_id; +} + +void simple_wallet::mms_init(const std::vector<std::string> &args) +{ + if (args.size() != 3) + { + fail_msg_writer() << tr("usage: mms init <required_signers>/<authorized_signers> <own_label> <own_transport_address>"); + return; + } + mms::message_store& ms = m_wallet->get_message_store(); + if (ms.get_active()) + { + if (!user_confirms(tr("The MMS is already initialized. Re-initialize by deleting all signer info and messages?"))) + { + return; + } + } + uint32_t num_required_signers; + uint32_t num_authorized_signers; + const std::string &mn = args[0]; + std::vector<std::string> numbers; + boost::split(numbers, mn, boost::is_any_of("/")); + bool mn_ok = (numbers.size() == 2) + && get_number_from_arg(numbers[1], num_authorized_signers, 2, 100) + && get_number_from_arg(numbers[0], num_required_signers, 2, num_authorized_signers); + if (!mn_ok) + { + fail_msg_writer() << tr("Error in the number of required signers and/or authorized signers"); + return; + } + LOCK_IDLE_SCOPE(); + ms.init(get_multisig_wallet_state(), args[1], args[2], num_authorized_signers, num_required_signers); +} + +void simple_wallet::mms_info(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + if (ms.get_active()) + { + message_writer() << boost::format("The MMS is active for %s/%s multisig.") + % ms.get_num_required_signers() % ms.get_num_authorized_signers(); + } + else + { + message_writer() << tr("The MMS is not active."); + } +} + +void simple_wallet::mms_signer(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + const std::vector<mms::authorized_signer> &signers = ms.get_all_signers(); + if (args.size() == 0) + { + // Without further parameters list all defined signers + list_signers(signers); + return; + } + + uint32_t index; + bool index_valid = get_number_from_arg(args[0], index, 1, ms.get_num_authorized_signers()); + if (index_valid) + { + index--; + } + else + { + fail_msg_writer() << tr("Invalid signer number ") + args[0]; + return; + } + if ((args.size() < 2) || (args.size() > 4)) + { + fail_msg_writer() << tr("mms signer [<number> <label> [<transport_address> [<monero_address>]]]"); + return; + } + + boost::optional<string> label = args[1]; + boost::optional<string> transport_address; + if (args.size() >= 3) + { + transport_address = args[2]; + } + boost::optional<cryptonote::account_public_address> monero_address; + LOCK_IDLE_SCOPE(); + mms::multisig_wallet_state state = get_multisig_wallet_state(); + if (args.size() == 4) + { + cryptonote::address_parse_info info; + bool ok = cryptonote::get_account_address_from_str_or_url(info, m_wallet->nettype(), args[3], oa_prompter); + if (!ok) + { + fail_msg_writer() << tr("Invalid Monero address"); + return; + } + monero_address = info.address; + const std::vector<mms::message> &messages = ms.get_all_messages(); + if ((messages.size() > 0) || state.multisig) + { + fail_msg_writer() << tr("Wallet state does not allow changing Monero addresses anymore"); + return; + } + } + ms.set_signer(state, index, label, transport_address, monero_address); +} + +void simple_wallet::mms_list(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + if (args.size() != 0) + { + fail_msg_writer() << tr("Usage: mms list"); + return; + } + LOCK_IDLE_SCOPE(); + const std::vector<mms::message> &messages = ms.get_all_messages(); + list_mms_messages(messages); +} + +void simple_wallet::mms_next(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + if ((args.size() > 1) || ((args.size() == 1) && (args[0] != "sync"))) + { + fail_msg_writer() << tr("Usage: mms next [sync]"); + return; + } + bool avail = false; + std::vector<mms::processing_data> data_list; + bool force_sync = false; + uint32_t choice = 0; + { + LOCK_IDLE_SCOPE(); + if ((args.size() == 1) && (args[0] == "sync")) + { + // Force the MMS to process any waiting sync info although on its own it would just ignore + // those messages because no need to process them can be seen + force_sync = true; + } + string wait_reason; + { + avail = ms.get_processable_messages(get_multisig_wallet_state(), force_sync, data_list, wait_reason); + } + if (avail) + { + avail = choose_mms_processing(data_list, choice); + } + else if (!wait_reason.empty()) + { + message_writer() << tr("No next step: ") << wait_reason; + } + } + if (avail) + { + mms::processing_data data = data_list[choice]; + bool command_successful = false; + switch(data.processing) + { + case mms::message_processing::prepare_multisig: + message_writer() << tr("prepare_multisig"); + command_successful = prepare_multisig_main(std::vector<std::string>(), true); + break; + + case mms::message_processing::make_multisig: + { + message_writer() << tr("make_multisig"); + size_t number_of_key_sets = data.message_ids.size(); + std::vector<std::string> sig_args(number_of_key_sets + 1); + sig_args[0] = std::to_string(ms.get_num_required_signers()); + for (size_t i = 0; i < number_of_key_sets; ++i) + { + mms::message m = ms.get_message_by_id(data.message_ids[i]); + sig_args[i+1] = m.content; + } + command_successful = make_multisig_main(sig_args, true); + break; + } + + case mms::message_processing::exchange_multisig_keys: + { + message_writer() << tr("exchange_multisig_keys"); + size_t number_of_key_sets = data.message_ids.size(); + // Other than "make_multisig" only the key sets as parameters, no num_required_signers + std::vector<std::string> sig_args(number_of_key_sets); + for (size_t i = 0; i < number_of_key_sets; ++i) + { + mms::message m = ms.get_message_by_id(data.message_ids[i]); + sig_args[i] = m.content; + } + command_successful = exchange_multisig_keys_main(sig_args, true); + break; + } + + case mms::message_processing::create_sync_data: + { + message_writer() << tr("export_multisig_info"); + std::vector<std::string> export_args; + export_args.push_back("MMS"); // dummy filename + command_successful = export_multisig_main(export_args, true); + break; + } + + case mms::message_processing::process_sync_data: + { + message_writer() << tr("import_multisig_info"); + std::vector<std::string> import_args; + for (size_t i = 0; i < data.message_ids.size(); ++i) + { + mms::message m = ms.get_message_by_id(data.message_ids[i]); + import_args.push_back(m.content); + } + command_successful = import_multisig_main(import_args, true); + break; + } + + case mms::message_processing::sign_tx: + { + message_writer() << tr("sign_multisig"); + std::vector<std::string> sign_args; + mms::message m = ms.get_message_by_id(data.message_ids[0]); + sign_args.push_back(m.content); + command_successful = sign_multisig_main(sign_args, true); + break; + } + + case mms::message_processing::submit_tx: + { + message_writer() << tr("submit_multisig"); + std::vector<std::string> submit_args; + mms::message m = ms.get_message_by_id(data.message_ids[0]); + submit_args.push_back(m.content); + command_successful = submit_multisig_main(submit_args, true); + break; + } + + case mms::message_processing::send_tx: + { + message_writer() << tr("Send tx"); + mms::message m = ms.get_message_by_id(data.message_ids[0]); + LOCK_IDLE_SCOPE(); + ms.add_message(get_multisig_wallet_state(), data.receiving_signer_index, m.type, mms::message_direction::out, + m.content); + command_successful = true; + break; + } + + case mms::message_processing::process_signer_config: + { + message_writer() << tr("Process signer config"); + LOCK_IDLE_SCOPE(); + mms::message m = ms.get_message_by_id(data.message_ids[0]); + mms::authorized_signer me = ms.get_signer(0); + mms::multisig_wallet_state state = get_multisig_wallet_state(); + if (!me.auto_config_running) + { + // If no auto-config is running, the config sent may be unsolicited or problematic + // so show what arrived and ask for confirmation before taking it in + std::vector<mms::authorized_signer> signers; + ms.unpack_signer_config(state, m.content, signers); + list_signers(signers); + if (!user_confirms(tr("Replace current signer config with the one displayed above?"))) + { + break; + } + } + ms.process_signer_config(state, m.content); + ms.stop_auto_config(); + list_signers(ms.get_all_signers()); + command_successful = true; + break; + } + + case mms::message_processing::process_auto_config_data: + { + message_writer() << tr("Process auto config data"); + LOCK_IDLE_SCOPE(); + for (size_t i = 0; i < data.message_ids.size(); ++i) + { + ms.process_auto_config_data_message(data.message_ids[i]); + } + ms.stop_auto_config(); + list_signers(ms.get_all_signers()); + add_signer_config_messages(); + command_successful = true; + break; + } + + default: + message_writer() << tr("Nothing ready to process"); + break; + } + + if (command_successful) + { + { + LOCK_IDLE_SCOPE(); + ms.set_messages_processed(data); + ask_send_all_ready_messages(); + } + } + } +} + +void simple_wallet::mms_sync(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + if (args.size() != 0) + { + fail_msg_writer() << tr("Usage: mms sync"); + return; + } + // Force the start of a new sync round, for exceptional cases where something went wrong + // Can e.g. solve the problem "This signature was made with stale data" after trying to + // create 2 transactions in a row somehow + // Code is identical to the code for 'message_processing::create_sync_data' + message_writer() << tr("export_multisig_info"); + std::vector<std::string> export_args; + export_args.push_back("MMS"); // dummy filename + export_multisig_main(export_args, true); + ask_send_all_ready_messages(); +} + +void simple_wallet::mms_transfer(const std::vector<std::string> &args) +{ + // It's too complicated to check any arguments here, just let 'transfer_main' do the whole job + transfer_main(Transfer, args, true); +} + +void simple_wallet::mms_delete(const std::vector<std::string> &args) +{ + if (args.size() != 1) + { + fail_msg_writer() << tr("Usage: mms delete (<message_id> | all)"); + return; + } + LOCK_IDLE_SCOPE(); + mms::message_store& ms = m_wallet->get_message_store(); + if (args[0] == "all") + { + if (user_confirms(tr("Delete all messages?"))) + { + ms.delete_all_messages(); + } + } + else + { + mms::message m; + bool valid_id = get_message_from_arg(args[0], m); + if (valid_id) + { + // If only a single message and not all delete even if unsent / unprocessed + ms.delete_message(m.id); + } + } +} + +void simple_wallet::mms_send(const std::vector<std::string> &args) +{ + if (args.size() == 0) + { + ask_send_all_ready_messages(); + return; + } + else if (args.size() != 1) + { + fail_msg_writer() << tr("Usage: mms send [<message_id>]"); + return; + } + LOCK_IDLE_SCOPE(); + mms::message_store& ms = m_wallet->get_message_store(); + mms::message m; + bool valid_id = get_message_from_arg(args[0], m); + if (valid_id) + { + ms.send_message(get_multisig_wallet_state(), m.id); + } +} + +void simple_wallet::mms_receive(const std::vector<std::string> &args) +{ + if (args.size() != 0) + { + fail_msg_writer() << tr("Usage: mms receive"); + return; + } + std::vector<mms::message> new_messages; + LOCK_IDLE_SCOPE(); + mms::message_store& ms = m_wallet->get_message_store(); + bool avail = ms.check_for_messages(get_multisig_wallet_state(), new_messages); + if (avail) + { + list_mms_messages(new_messages); + } +} + +void simple_wallet::mms_export(const std::vector<std::string> &args) +{ + if (args.size() != 1) + { + fail_msg_writer() << tr("Usage: mms export <message_id>"); + return; + } + LOCK_IDLE_SCOPE(); + mms::message_store& ms = m_wallet->get_message_store(); + mms::message m; + bool valid_id = get_message_from_arg(args[0], m); + if (valid_id) + { + const std::string filename = "mms_message_content"; + if (epee::file_io_utils::save_string_to_file(filename, m.content)) + { + success_msg_writer() << tr("Message content saved to: ") << filename; + } + else + { + fail_msg_writer() << tr("Failed to to save message content"); + } + } +} + +void simple_wallet::mms_note(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + if (args.size() == 0) + { + LOCK_IDLE_SCOPE(); + const std::vector<mms::message> &messages = ms.get_all_messages(); + for (size_t i = 0; i < messages.size(); ++i) + { + const mms::message &m = messages[i]; + if ((m.type == mms::message_type::note) && (m.state == mms::message_state::waiting)) + { + show_message(m); + } + } + return; + } + if (args.size() < 2) + { + fail_msg_writer() << tr("Usage: mms note [<label> <text>]"); + return; + } + uint32_t signer_index; + bool found = ms.get_signer_index_by_label(args[0], signer_index); + if (!found) + { + fail_msg_writer() << tr("No signer found with label ") << args[0]; + return; + } + std::string note = ""; + for (size_t n = 1; n < args.size(); ++n) + { + if (n > 1) + { + note += " "; + } + note += args[n]; + } + LOCK_IDLE_SCOPE(); + ms.add_message(get_multisig_wallet_state(), signer_index, mms::message_type::note, + mms::message_direction::out, note); + ask_send_all_ready_messages(); +} + +void simple_wallet::mms_show(const std::vector<std::string> &args) +{ + if (args.size() != 1) + { + fail_msg_writer() << tr("Usage: mms show <message_id>"); + return; + } + LOCK_IDLE_SCOPE(); + mms::message_store& ms = m_wallet->get_message_store(); + mms::message m; + bool valid_id = get_message_from_arg(args[0], m); + if (valid_id) + { + show_message(m); + } +} + +void simple_wallet::mms_set(const std::vector<std::string> &args) +{ + bool set = args.size() == 2; + bool query = args.size() == 1; + if (!set && !query) + { + fail_msg_writer() << tr("Usage: mms set <option_name> [<option_value>]"); + return; + } + mms::message_store& ms = m_wallet->get_message_store(); + LOCK_IDLE_SCOPE(); + if (args[0] == "auto-send") + { + if (set) + { + bool result; + bool ok = parse_bool(args[1], result); + if (ok) + { + ms.set_auto_send(result); + } + else + { + fail_msg_writer() << tr("Wrong option value"); + } + } + else + { + message_writer() << (ms.get_auto_send() ? tr("Auto-send is on") : tr("Auto-send is off")); + } + } + else + { + fail_msg_writer() << tr("Unknown option"); + } +} + +void simple_wallet::mms_help(const std::vector<std::string> &args) +{ + if (args.size() > 1) + { + fail_msg_writer() << tr("Usage: mms help [<subcommand>]"); + return; + } + std::vector<std::string> help_args; + help_args.push_back("mms"); + if (args.size() == 1) + { + help_args.push_back(args[0]); + } + help(help_args); +} + +void simple_wallet::mms_send_signer_config(const std::vector<std::string> &args) +{ + if (args.size() != 0) + { + fail_msg_writer() << tr("Usage: mms send_signer_config"); + return; + } + mms::message_store& ms = m_wallet->get_message_store(); + if (!ms.signer_config_complete()) + { + fail_msg_writer() << tr("Signer config not yet complete"); + return; + } + LOCK_IDLE_SCOPE(); + add_signer_config_messages(); + ask_send_all_ready_messages(); +} + +void simple_wallet::mms_start_auto_config(const std::vector<std::string> &args) +{ + mms::message_store& ms = m_wallet->get_message_store(); + uint32_t other_signers = ms.get_num_authorized_signers() - 1; + size_t args_size = args.size(); + if ((args_size != 0) && (args_size != other_signers)) + { + fail_msg_writer() << tr("Usage: mms start_auto_config [<label> <label> ...]"); + return; + } + if ((args_size == 0) && !ms.signer_labels_complete()) + { + fail_msg_writer() << tr("There are signers without a label set. Complete labels before auto-config or specify them as parameters here."); + return; + } + mms::authorized_signer me = ms.get_signer(0); + if (me.auto_config_running) + { + if (!user_confirms(tr("Auto-config is already running. Cancel and restart?"))) + { + return; + } + } + LOCK_IDLE_SCOPE(); + mms::multisig_wallet_state state = get_multisig_wallet_state(); + if (args_size != 0) + { + // Set (or overwrite) all the labels except "me" from the arguments + for (uint32_t i = 1; i < (other_signers + 1); ++i) + { + ms.set_signer(state, i, args[i - 1], boost::none, boost::none); + } + } + ms.start_auto_config(state); + // List the signers to show the generated auto-config tokens + list_signers(ms.get_all_signers()); +} + +void simple_wallet::mms_stop_auto_config(const std::vector<std::string> &args) +{ + if (args.size() != 0) + { + fail_msg_writer() << tr("Usage: mms stop_auto_config"); + return; + } + if (!user_confirms(tr("Delete any auto-config tokens and stop auto-config?"))) + { + return; + } + mms::message_store& ms = m_wallet->get_message_store(); + LOCK_IDLE_SCOPE(); + ms.stop_auto_config(); +} + +void simple_wallet::mms_auto_config(const std::vector<std::string> &args) +{ + if (args.size() != 1) + { + fail_msg_writer() << tr("Usage: mms auto_config <auto_config_token>"); + return; + } + mms::message_store& ms = m_wallet->get_message_store(); + std::string adjusted_token; + if (!ms.check_auto_config_token(args[0], adjusted_token)) + { + fail_msg_writer() << tr("Invalid auto-config token"); + return; + } + mms::authorized_signer me = ms.get_signer(0); + if (me.auto_config_running) + { + if (!user_confirms(tr("Auto-config already running. Cancel and restart?"))) + { + return; + } + } + LOCK_IDLE_SCOPE(); + ms.add_auto_config_data_message(get_multisig_wallet_state(), adjusted_token); + ask_send_all_ready_messages(); +} + +bool simple_wallet::mms(const std::vector<std::string> &args) +{ + try + { + mms::message_store& ms = m_wallet->get_message_store(); + if (args.size() == 0) + { + mms_info(args); + return true; + } + + const std::string &sub_command = args[0]; + std::vector<std::string> mms_args = args; + mms_args.erase(mms_args.begin()); + + if (sub_command == "init") + { + mms_init(mms_args); + return true; + } + if (!ms.get_active()) + { + fail_msg_writer() << tr("The MMS is not active. Activate using the \"mms init\" command"); + return true; + } + else if (sub_command == "info") + { + mms_info(mms_args); + } + else if (sub_command == "signer") + { + mms_signer(mms_args); + } + else if (sub_command == "list") + { + mms_list(mms_args); + } + else if (sub_command == "next") + { + mms_next(mms_args); + } + else if (sub_command == "sync") + { + mms_sync(mms_args); + } + else if (sub_command == "transfer") + { + mms_transfer(mms_args); + } + else if (sub_command == "delete") + { + mms_delete(mms_args); + } + else if (sub_command == "send") + { + mms_send(mms_args); + } + else if (sub_command == "receive") + { + mms_receive(mms_args); + } + else if (sub_command == "export") + { + mms_export(mms_args); + } + else if (sub_command == "note") + { + mms_note(mms_args); + } + else if (sub_command == "show") + { + mms_show(mms_args); + } + else if (sub_command == "set") + { + mms_set(mms_args); + } + else if (sub_command == "help") + { + mms_help(mms_args); + } + else if (sub_command == "send_signer_config") + { + mms_send_signer_config(mms_args); + } + else if (sub_command == "start_auto_config") + { + mms_start_auto_config(mms_args); + } + else if (sub_command == "stop_auto_config") + { + mms_stop_auto_config(mms_args); + } + else if (sub_command == "auto_config") + { + mms_auto_config(mms_args); + } + else + { + fail_msg_writer() << tr("Invalid MMS subcommand"); + } + } + catch (const tools::error::no_connection_to_daemon &e) + { + fail_msg_writer() << tr("Error in MMS command: ") << e.what() << " " << e.request(); + } + catch (const std::exception &e) + { + fail_msg_writer() << tr("Error in MMS command: ") << e.what(); + return true; + } + return true; +} +// End MMS ------------------------------------------------------------------------------------------------ + diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 665879cac..5010e3adc 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -154,7 +154,7 @@ namespace cryptonote bool show_incoming_transfers(const std::vector<std::string> &args); bool show_payments(const std::vector<std::string> &args); bool show_blockchain_height(const std::vector<std::string> &args); - bool transfer_main(int transfer_type, const std::vector<std::string> &args); + bool transfer_main(int transfer_type, const std::vector<std::string> &args, bool called_by_mms); bool transfer(const std::vector<std::string> &args); bool locked_transfer(const std::vector<std::string> &args); bool locked_sweep_all(const std::vector<std::string> &args); @@ -214,15 +214,23 @@ namespace cryptonote bool payment_id(const std::vector<std::string> &args); bool print_fee_info(const std::vector<std::string> &args); bool prepare_multisig(const std::vector<std::string>& args); + bool prepare_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool make_multisig(const std::vector<std::string>& args); + bool make_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool finalize_multisig(const std::vector<std::string> &args); bool exchange_multisig_keys(const std::vector<std::string> &args); + bool exchange_multisig_keys_main(const std::vector<std::string> &args, bool called_by_mms); bool export_multisig(const std::vector<std::string>& args); + bool export_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool import_multisig(const std::vector<std::string>& args); + bool import_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool accept_loaded_tx(const tools::wallet2::multisig_tx_set &txs); bool sign_multisig(const std::vector<std::string>& args); + bool sign_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool submit_multisig(const std::vector<std::string>& args); + bool submit_multisig_main(const std::vector<std::string>& args, bool called_by_mms); bool export_raw_multisig(const std::vector<std::string>& args); + bool mms(const std::vector<std::string>& args); bool print_ring(const std::vector<std::string>& args); bool set_ring(const std::vector<std::string>& args); bool save_known_rings(const std::vector<std::string>& args); @@ -386,5 +394,40 @@ namespace cryptonote bool m_auto_refresh_refreshing; std::atomic<bool> m_in_manual_refresh; uint32_t m_current_subaddress_account; + + // MMS + mms::message_store& get_message_store() const { return m_wallet->get_message_store(); }; + mms::multisig_wallet_state get_multisig_wallet_state() const { return m_wallet->get_multisig_wallet_state(); }; + bool mms_active() const { return get_message_store().get_active(); }; + bool choose_mms_processing(const std::vector<mms::processing_data> &data_list, uint32_t &choice); + void list_mms_messages(const std::vector<mms::message> &messages); + void list_signers(const std::vector<mms::authorized_signer> &signers); + void add_signer_config_messages(); + void show_message(const mms::message &m); + void ask_send_all_ready_messages(); + void check_for_messages(); + bool user_confirms(const std::string &question); + bool get_message_from_arg(const std::string &arg, mms::message &m); + bool get_number_from_arg(const std::string &arg, uint32_t &number, const uint32_t lower_bound, const uint32_t upper_bound); + + void mms_init(const std::vector<std::string> &args); + void mms_info(const std::vector<std::string> &args); + void mms_signer(const std::vector<std::string> &args); + void mms_list(const std::vector<std::string> &args); + void mms_next(const std::vector<std::string> &args); + void mms_sync(const std::vector<std::string> &args); + void mms_transfer(const std::vector<std::string> &args); + void mms_delete(const std::vector<std::string> &args); + void mms_send(const std::vector<std::string> &args); + void mms_receive(const std::vector<std::string> &args); + void mms_export(const std::vector<std::string> &args); + void mms_note(const std::vector<std::string> &args); + void mms_show(const std::vector<std::string> &args); + void mms_set(const std::vector<std::string> &args); + void mms_help(const std::vector<std::string> &args); + void mms_send_signer_config(const std::vector<std::string> &args); + void mms_start_auto_config(const std::vector<std::string> &args); + void mms_stop_auto_config(const std::vector<std::string> &args); + void mms_auto_config(const std::vector<std::string> &args); }; } diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 4e3fb1ae5..8425e4a03 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -34,7 +34,10 @@ set(wallet_sources wallet2.cpp wallet_args.cpp ringdb.cpp - node_rpc_proxy.cpp) + node_rpc_proxy.cpp + message_store.cpp + message_transporter.cpp +) set(wallet_private_headers wallet2.h @@ -44,7 +47,9 @@ set(wallet_private_headers wallet_rpc_server_commands_defs.h wallet_rpc_server_error_codes.h ringdb.h - node_rpc_proxy.h) + node_rpc_proxy.h + message_store.h + message_transporter.h) monero_private_headers(wallet ${wallet_private_headers}) diff --git a/src/wallet/message_store.cpp b/src/wallet/message_store.cpp new file mode 100644 index 000000000..ce6f4f52b --- /dev/null +++ b/src/wallet/message_store.cpp @@ -0,0 +1,1445 @@ +// Copyright (c) 2018, 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 "message_store.h" +#include <boost/archive/portable_binary_oarchive.hpp> +#include <boost/archive/portable_binary_iarchive.hpp> +#include <boost/format.hpp> +#include <boost/algorithm/string.hpp> +#include <fstream> +#include <sstream> +#include "file_io_utils.h" +#include "storages/http_abstract_invoke.h" +#include "wallet_errors.h" +#include "serialization/binary_utils.h" +#include "common/base58.h" +#include "common/util.h" +#include "string_tools.h" + + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.mms" + +namespace mms +{ + +message_store::message_store() +{ + m_active = false; + m_auto_send = false; + m_next_message_id = 1; + m_num_authorized_signers = 0; + m_num_required_signers = 0; + m_nettype = cryptonote::network_type::UNDEFINED; + m_run = true; +} + +namespace +{ + // MMS options handling mirrors what "wallet2" is doing for its options, on-demand init and all + // It's not very clean to initialize Bitmessage-specific options here, but going one level further + // down still into "message_transporter" for that is a little bit too much + struct options + { + const command_line::arg_descriptor<std::string> bitmessage_address = {"bitmessage-address", mms::message_store::tr("Use PyBitmessage instance at URL <arg>"), "http://localhost:8442/"}; + const command_line::arg_descriptor<std::string> bitmessage_login = {"bitmessage-login", mms::message_store::tr("Specify <arg> as username:password for PyBitmessage API"), "username:password"}; + }; +} + +void message_store::init_options(boost::program_options::options_description& desc_params) +{ + const options opts{}; + command_line::add_arg(desc_params, opts.bitmessage_address); + command_line::add_arg(desc_params, opts.bitmessage_login); +} + +void message_store::init(const multisig_wallet_state &state, const std::string &own_label, + const std::string &own_transport_address, uint32_t num_authorized_signers, uint32_t num_required_signers) +{ + m_num_authorized_signers = num_authorized_signers; + m_num_required_signers = num_required_signers; + m_signers.clear(); + m_messages.clear(); + m_next_message_id = 1; + + // The vector "m_signers" gets here once the required number of elements, one for each authorized signer, + // and is never changed again. The rest of the code relies on "size(m_signers) == m_num_authorized_signers" + // without further checks. + authorized_signer signer; + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + signer.me = signer.index == 0; // Strict convention: The very first signer is fixed as / must be "me" + m_signers.push_back(signer); + signer.index++; + } + + set_signer(state, 0, own_label, own_transport_address, state.address); + + m_nettype = state.nettype; + set_active(true); + m_filename = state.mms_file; + save(state); +} + +void message_store::set_options(const boost::program_options::variables_map& vm) +{ + const options opts{}; + std::string bitmessage_address = command_line::get_arg(vm, opts.bitmessage_address); + epee::wipeable_string bitmessage_login = command_line::get_arg(vm, opts.bitmessage_login); + set_options(bitmessage_address, bitmessage_login); +} + +void message_store::set_options(const std::string &bitmessage_address, const epee::wipeable_string &bitmessage_login) +{ + m_transporter.set_options(bitmessage_address, bitmessage_login); +} + +void message_store::set_signer(const multisig_wallet_state &state, + uint32_t index, + const boost::optional<std::string> &label, + const boost::optional<std::string> &transport_address, + const boost::optional<cryptonote::account_public_address> monero_address) +{ + THROW_WALLET_EXCEPTION_IF(index >= m_num_authorized_signers, tools::error::wallet_internal_error, "Invalid signer index " + index); + authorized_signer &m = m_signers[index]; + if (label) + { + m.label = label.get(); + } + if (transport_address) + { + m.transport_address = transport_address.get(); + } + if (monero_address) + { + m.monero_address_known = true; + m.monero_address = monero_address.get(); + } + // Save to minimize the chance to loose that info (at least while in beta) + save(state); +} + +const authorized_signer &message_store::get_signer(uint32_t index) const +{ + THROW_WALLET_EXCEPTION_IF(index >= m_num_authorized_signers, tools::error::wallet_internal_error, "Invalid signer index " + index); + return m_signers[index]; +} + +bool message_store::signer_config_complete() const +{ + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = m_signers[i]; + if (m.label.empty() || m.transport_address.empty() || !m.monero_address_known) + { + return false; + } + } + return true; +} + +// Check if all signers have a label set (as it's a requirement for starting auto-config +// by the "manager") +bool message_store::signer_labels_complete() const +{ + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = m_signers[i]; + if (m.label.empty()) + { + return false; + } + } + return true; +} + +void message_store::get_signer_config(std::string &signer_config) +{ + std::stringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + ar << m_signers; + signer_config = oss.str(); +} + +void message_store::unpack_signer_config(const multisig_wallet_state &state, const std::string &signer_config, + std::vector<authorized_signer> &signers) +{ + try + { + std::stringstream iss; + iss << signer_config; + boost::archive::portable_binary_iarchive ar(iss); + ar >> signers; + } + catch (...) + { + THROW_WALLET_EXCEPTION_IF(true, tools::error::wallet_internal_error, "Invalid structure of signer config"); + } + uint32_t num_signers = (uint32_t)signers.size(); + THROW_WALLET_EXCEPTION_IF(num_signers != m_num_authorized_signers, tools::error::wallet_internal_error, "Wrong number of signers in config: " + num_signers); +} + +void message_store::process_signer_config(const multisig_wallet_state &state, const std::string &signer_config) +{ + // The signers in "signer_config" and the resident wallet signers are matched not by label, but + // by Monero address, and ALL labels will be set from "signer_config", even the "me" label. + // In the auto-config process as implemented now the auto-config manager is responsible for defining + // the labels, and right at the end of the process ALL wallets use the SAME labels. The idea behind this + // is preventing problems like duplicate labels and confusion (Bob choosing a label "IamAliceHonest"). + // (Of course signers are free to re-define any labels they don't like AFTER auto-config.) + // + // Usually this method will be called with only the "me" signer defined in the wallet, and may + // produce unexpected behaviour if that wallet contains additional signers that have nothing to do with + // those arriving in "signer_config". + std::vector<authorized_signer> signers; + unpack_signer_config(state, signer_config, signers); + + uint32_t new_index = 1; + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = signers[i]; + uint32_t index; + uint32_t take_index; + bool found = get_signer_index_by_monero_address(m.monero_address, index); + if (found) + { + // Redefine existing (probably "me", under usual circumstances) + take_index = index; + } + else + { + // Add new; neglect that we may erroneously overwrite already defined signers + // (but protect "me") + take_index = new_index; + if ((new_index + 1) < m_num_authorized_signers) + { + new_index++; + } + } + authorized_signer &modify = m_signers[take_index]; + modify.label = m.label; // ALWAYS set label, see comments above + if (!modify.me) + { + modify.transport_address = m.transport_address; + modify.monero_address_known = m.monero_address_known; + if (m.monero_address_known) + { + modify.monero_address = m.monero_address; + } + } + } + save(state); +} + +void message_store::start_auto_config(const multisig_wallet_state &state) +{ + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + authorized_signer &m = m_signers[i]; + if (!m.me) + { + setup_signer_for_auto_config(i, create_auto_config_token(), true); + } + m.auto_config_running = true; + } + save(state); +} + +// Check auto-config token string and convert to standardized form; +// Try to make it as foolproof as possible, with built-in tolerance to make up for +// errors in transmission that still leave the token recognizable. +bool message_store::check_auto_config_token(const std::string &raw_token, + std::string &adjusted_token) const +{ + std::string prefix(AUTO_CONFIG_TOKEN_PREFIX); + uint32_t num_hex_digits = (AUTO_CONFIG_TOKEN_BYTES + 1) * 2; + uint32_t full_length = num_hex_digits + prefix.length(); + uint32_t raw_length = raw_token.length(); + std::string hex_digits; + + if (raw_length == full_length) + { + // Prefix must be there; accept it in any casing + std::string raw_prefix(raw_token.substr(0, 3)); + boost::algorithm::to_lower(raw_prefix); + if (raw_prefix != prefix) + { + return false; + } + hex_digits = raw_token.substr(3); + } + else if (raw_length == num_hex_digits) + { + // Accept the token without the prefix if it's otherwise ok + hex_digits = raw_token; + } + else + { + return false; + } + + // Convert to strict lowercase and correct any common misspellings + boost::algorithm::to_lower(hex_digits); + std::replace(hex_digits.begin(), hex_digits.end(), 'o', '0'); + std::replace(hex_digits.begin(), hex_digits.end(), 'i', '1'); + std::replace(hex_digits.begin(), hex_digits.end(), 'l', '1'); + + // Now it must be correct hex with correct checksum, no further tolerance possible + std::string token_bytes; + if (!epee::string_tools::parse_hexstr_to_binbuff(hex_digits, token_bytes)) + { + return false; + } + const crypto::hash &hash = crypto::cn_fast_hash(token_bytes.data(), token_bytes.size() - 1); + if (token_bytes[AUTO_CONFIG_TOKEN_BYTES] != hash.data[0]) + { + return false; + } + adjusted_token = prefix + hex_digits; + return true; +} + +// Create a new auto-config token with prefix, random 8-hex digits plus 2 checksum digits +std::string message_store::create_auto_config_token() +{ + unsigned char random[AUTO_CONFIG_TOKEN_BYTES]; + crypto::rand(AUTO_CONFIG_TOKEN_BYTES, random); + std::string token_bytes; + token_bytes.append((char *)random, AUTO_CONFIG_TOKEN_BYTES); + + // Add a checksum because technically ANY four bytes are a valid token, and without a checksum we would send + // auto-config messages "to nowhere" after the slightest typo without knowing it + const crypto::hash &hash = crypto::cn_fast_hash(token_bytes.data(), token_bytes.size()); + token_bytes += hash.data[0]; + std::string prefix(AUTO_CONFIG_TOKEN_PREFIX); + return prefix + epee::string_tools::buff_to_hex_nodelimer(token_bytes); +} + +// Add a message for sending "me" address data to the auto-config transport address +// that can be derived from the token and activate auto-config +size_t message_store::add_auto_config_data_message(const multisig_wallet_state &state, + const std::string &auto_config_token) +{ + authorized_signer &me = m_signers[0]; + me.auto_config_token = auto_config_token; + setup_signer_for_auto_config(0, auto_config_token, false); + me.auto_config_running = true; + + auto_config_data data; + data.label = me.label; + data.transport_address = me.transport_address; + data.monero_address = me.monero_address; + + std::stringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + ar << data; + + return add_message(state, 0, message_type::auto_config_data, message_direction::out, oss.str()); +} + +// Process a single message with auto-config data, destined for "message.signer_index" +void message_store::process_auto_config_data_message(uint32_t id) +{ + // "auto_config_data" contains the label that the auto-config data sender uses for "me", but that's + // more for completeness' sake, and right now it's not used. In general, the auto-config manager + // decides/defines the labels, and right after completing auto-config ALL wallets use the SAME labels. + + const message &m = get_message_ref_by_id(id); + + auto_config_data data; + try + { + std::stringstream iss; + iss << m.content; + boost::archive::portable_binary_iarchive ar(iss); + ar >> data; + } + catch (...) + { + THROW_WALLET_EXCEPTION_IF(true, tools::error::wallet_internal_error, "Invalid structure of auto config data"); + } + + authorized_signer &signer = m_signers[m.signer_index]; + // "signer.label" does NOT change, see comment above + signer.transport_address = data.transport_address; + signer.monero_address_known = true; + signer.monero_address = data.monero_address; + signer.auto_config_running = false; +} + +void message_store::stop_auto_config() +{ + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + authorized_signer &m = m_signers[i]; + if (!m.me && !m.auto_config_transport_address.empty()) + { + // Try to delete those "unused API" addresses in PyBitmessage, especially since + // it seems it's not possible to delete them interactively, only to "disable" them + m_transporter.delete_transport_address(m.auto_config_transport_address); + } + m.auto_config_token.clear(); + m.auto_config_public_key = crypto::null_pkey; + m.auto_config_secret_key = crypto::null_skey; + m.auto_config_transport_address.clear(); + m.auto_config_running = false; + } +} + +void message_store::setup_signer_for_auto_config(uint32_t index, const std::string token, bool receiving) +{ + // It may be a little strange to hash the textual hex digits of the auto config token into + // 32 bytes and turn that into a Monero public/secret key pair, instead of doing something + // much less complicated like directly using the underlying random 40 bits as key for a + // symmetric cipher, but everything is there already for encrypting and decrypting messages + // with such key pairs, and furthermore it would be trivial to use tokens with a different + // number of bytes. + // + // In the wallet of the auto-config manager each signer except "me" gets set its own + // auto-config parameters. In the wallet of somebody using the token to send auto-config + // data the auto-config parameters are stored in the "me" signer and taken from there + // to send that data. + THROW_WALLET_EXCEPTION_IF(index >= m_num_authorized_signers, tools::error::wallet_internal_error, "Invalid signer index " + index); + authorized_signer &m = m_signers[index]; + m.auto_config_token = token; + crypto::hash_to_scalar(token.data(), token.size(), m.auto_config_secret_key); + crypto::secret_key_to_public_key(m.auto_config_secret_key, m.auto_config_public_key); + if (receiving) + { + m.auto_config_transport_address = m_transporter.derive_and_receive_transport_address(m.auto_config_token); + } + else + { + m.auto_config_transport_address = m_transporter.derive_transport_address(m.auto_config_token); + } +} + +bool message_store::get_signer_index_by_monero_address(const cryptonote::account_public_address &monero_address, uint32_t &index) const +{ + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = m_signers[i]; + if (m.monero_address == monero_address) + { + index = m.index; + return true; + } + } + MWARNING("No authorized signer with Monero address " << account_address_to_string(monero_address)); + return false; +} + +bool message_store::get_signer_index_by_label(const std::string label, uint32_t &index) const +{ + for (uint32_t i = 0; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = m_signers[i]; + if (m.label == label) + { + index = m.index; + return true; + } + } + MWARNING("No authorized signer with label " << label); + return false; +} + +void message_store::process_wallet_created_data(const multisig_wallet_state &state, message_type type, const std::string &content) +{ + switch(type) + { + case message_type::key_set: + // Result of a "prepare_multisig" command in the wallet + // Send the key set to all other signers + case message_type::additional_key_set: + // Result of a "make_multisig" command or a "exchange_multisig_keys" in the wallet in case of M/N multisig + // Send the additional key set to all other signers + case message_type::multisig_sync_data: + // Result of a "export_multisig_info" command in the wallet + // Send the sync data to all other signers + for (uint32_t i = 1; i < m_num_authorized_signers; ++i) + { + add_message(state, i, type, message_direction::out, content); + } + break; + + case message_type::partially_signed_tx: + // Result of a "transfer" command in the wallet, or a "sign_multisig" command + // that did not yet result in the minimum number of signatures required + // Create a message "from me to me" as a container for the tx data + if (m_num_required_signers == 1) + { + // Probably rare, but possible: The 1 signature is already enough, correct the type + // Easier to correct here than asking all callers to detect this rare special case + type = message_type::fully_signed_tx; + } + add_message(state, 0, type, message_direction::in, content); + break; + + case message_type::fully_signed_tx: + add_message(state, 0, type, message_direction::in, content); + break; + + default: + THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, "Illegal message type " + (uint32_t)type); + break; + } +} + +size_t message_store::add_message(const multisig_wallet_state &state, + uint32_t signer_index, message_type type, message_direction direction, + const std::string &content) +{ + message m; + m.id = m_next_message_id++; + m.type = type; + m.direction = direction; + m.content = content; + m.created = (uint64_t)time(NULL); + m.modified = m.created; + m.sent = 0; + m.signer_index = signer_index; + if (direction == message_direction::out) + { + m.state = message_state::ready_to_send; + } + else + { + m.state = message_state::waiting; + }; + m.wallet_height = (uint32_t)state.num_transfer_details; + if (m.type == message_type::additional_key_set) + { + m.round = state.multisig_rounds_passed; + } + else + { + m.round = 0; + } + m.signature_count = 0; // Future expansion for signature counting when signing txs + m.hash = crypto::null_hash; + m_messages.push_back(m); + + // Save for every new message right away (at least while in beta) + save(state); + + MINFO(boost::format("Added %s message %s for signer %s of type %s") + % message_direction_to_string(direction) % m.id % signer_index % message_type_to_string(type)); + return m_messages.size() - 1; +} + +// Get the index of the message with id "id", return false if not found +bool message_store::get_message_index_by_id(uint32_t id, size_t &index) const +{ + for (size_t i = 0; i < m_messages.size(); ++i) + { + if (m_messages[i].id == id) + { + index = i; + return true; + } + } + MWARNING("No message found with an id of " << id); + return false; +} + +// Get the index of the message with id "id" that must exist +size_t message_store::get_message_index_by_id(uint32_t id) const +{ + size_t index; + bool found = get_message_index_by_id(id, index); + THROW_WALLET_EXCEPTION_IF(!found, tools::error::wallet_internal_error, "Invalid message id " + id); + return index; +} + +// Get the modifiable message with id "id" that must exist; private/internal use! +message& message_store::get_message_ref_by_id(uint32_t id) +{ + return m_messages[get_message_index_by_id(id)]; +} + +// Get the message with id "id", return false if not found +// This version of the method allows to check whether id is valid without triggering an error +bool message_store::get_message_by_id(uint32_t id, message &m) const +{ + size_t index; + bool found = get_message_index_by_id(id, index); + if (found) + { + m = m_messages[index]; + } + return found; +} + +// Get the message with id "id" that must exist +message message_store::get_message_by_id(uint32_t id) const +{ + message m; + bool found = get_message_by_id(id, m); + THROW_WALLET_EXCEPTION_IF(!found, tools::error::wallet_internal_error, "Invalid message id " + id); + return m; +} + +bool message_store::any_message_of_type(message_type type, message_direction direction) const +{ + for (size_t i = 0; i < m_messages.size(); ++i) + { + if ((m_messages[i].type == type) && (m_messages[i].direction == direction)) + { + return true; + } + } + return false; +} + +bool message_store::any_message_with_hash(const crypto::hash &hash) const +{ + for (size_t i = 0; i < m_messages.size(); ++i) + { + if (m_messages[i].hash == hash) + { + return true; + } + } + return false; +} + +// Count the ids in the vector that are set i.e. not 0, while ignoring index 0 +// Mostly used to check whether we have a message for each authorized signer except me, +// with the signer index used as index into 'ids'; the element at index 0, for me, +// is ignored, to make constant subtractions of 1 for indices when filling the +// vector unnecessary +size_t message_store::get_other_signers_id_count(const std::vector<uint32_t> &ids) const +{ + size_t count = 0; + for (size_t i = 1 /* and not 0 */; i < ids.size(); ++i) + { + if (ids[i] != 0) + { + count++; + } + } + return count; +} + +// Is in every element of vector 'ids' (except at index 0) a message id i.e. not 0? +bool message_store::message_ids_complete(const std::vector<uint32_t> &ids) const +{ + return get_other_signers_id_count(ids) == (ids.size() - 1); +} + +void message_store::delete_message(uint32_t id) +{ + delete_transport_message(id); + size_t index = get_message_index_by_id(id); + m_messages.erase(m_messages.begin() + index); +} + +void message_store::delete_all_messages() +{ + for (size_t i = 0; i < m_messages.size(); ++i) + { + delete_transport_message(m_messages[i].id); + } + m_messages.clear(); +} + +// Make a message text, which is "attacker controlled data", reasonably safe to display +// This is mostly geared towards the safe display of notes sent by "mms note" with a "mms show" command +void message_store::get_sanitized_message_text(const message &m, std::string &sanitized_text) const +{ + sanitized_text.clear(); + + // Restrict the size to fend of DOS-style attacks with heaps of data + size_t length = std::min(m.content.length(), (size_t)1000); + + for (size_t i = 0; i < length; ++i) + { + char c = m.content[i]; + if ((int)c < 32) + { + // Strip out any controls, especially ESC for getting rid of potentially dangerous + // ANSI escape sequences that a console window might interpret + c = ' '; + } + else if ((c == '<') || (c == '>')) + { + // Make XML or HTML impossible that e.g. might contain scripts that Qt might execute + // when displayed in the GUI wallet + c = ' '; + } + sanitized_text += c; + } +} + +void message_store::write_to_file(const multisig_wallet_state &state, const std::string &filename) +{ + std::stringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + ar << *this; + std::string buf = oss.str(); + + crypto::chacha_key key; + crypto::generate_chacha_key(&state.view_secret_key, sizeof(crypto::secret_key), key, 1); + + file_data write_file_data = boost::value_initialized<file_data>(); + write_file_data.magic_string = "MMS"; + write_file_data.file_version = 0; + write_file_data.iv = crypto::rand<crypto::chacha_iv>(); + std::string encrypted_data; + encrypted_data.resize(buf.size()); + crypto::chacha20(buf.data(), buf.size(), key, write_file_data.iv, &encrypted_data[0]); + write_file_data.encrypted_data = encrypted_data; + + std::stringstream file_oss; + boost::archive::portable_binary_oarchive file_ar(file_oss); + file_ar << write_file_data; + + bool success = epee::file_io_utils::save_string_to_file(filename, file_oss.str()); + THROW_WALLET_EXCEPTION_IF(!success, tools::error::file_save_error, filename); +} + +void message_store::read_from_file(const multisig_wallet_state &state, const std::string &filename) +{ + boost::system::error_code ignored_ec; + bool file_exists = boost::filesystem::exists(filename, ignored_ec); + if (!file_exists) + { + // Simply do nothing if the file is not there; allows e.g. easy recovery + // from problems with the MMS by deleting the file + MERROR("No message store file found: " << filename); + return; + } + + std::string buf; + bool success = epee::file_io_utils::load_file_to_string(filename, buf); + THROW_WALLET_EXCEPTION_IF(!success, tools::error::file_read_error, filename); + + file_data read_file_data; + try + { + std::stringstream iss; + iss << buf; + boost::archive::portable_binary_iarchive ar(iss); + ar >> read_file_data; + } + catch (const std::exception &e) + { + MERROR("MMS file " << filename << " has bad structure <iv,encrypted_data>: " << e.what()); + THROW_WALLET_EXCEPTION_IF(true, tools::error::file_read_error, filename); + } + + crypto::chacha_key key; + crypto::generate_chacha_key(&state.view_secret_key, sizeof(crypto::secret_key), key, 1); + std::string decrypted_data; + decrypted_data.resize(read_file_data.encrypted_data.size()); + crypto::chacha20(read_file_data.encrypted_data.data(), read_file_data.encrypted_data.size(), key, read_file_data.iv, &decrypted_data[0]); + + try + { + std::stringstream iss; + iss << decrypted_data; + boost::archive::portable_binary_iarchive ar(iss); + ar >> *this; + } + catch (const std::exception &e) + { + MERROR("MMS file " << filename << " has bad structure: " << e.what()); + THROW_WALLET_EXCEPTION_IF(true, tools::error::file_read_error, filename); + } + + m_filename = filename; +} + +// Save to the same file this message store was loaded from +// Called after changes deemed "important", to make it less probable to lose messages in case of +// a crash; a better and long-term solution would of course be to use LMDB ... +void message_store::save(const multisig_wallet_state &state) +{ + if (!m_filename.empty()) + { + write_to_file(state, m_filename); + } +} + +bool message_store::get_processable_messages(const multisig_wallet_state &state, + bool force_sync, std::vector<processing_data> &data_list, std::string &wait_reason) +{ + uint32_t wallet_height = (uint32_t)state.num_transfer_details; + data_list.clear(); + wait_reason.clear(); + // In all scans over all messages looking for complete sets (1 message for each signer), + // if there are duplicates, the OLDEST of them is taken. This may not play a role with + // any of the current message types, but may with future ones, and it's probably a good + // idea to have a clear and somewhat defensive strategy. + + std::vector<uint32_t> auto_config_messages(m_num_authorized_signers, 0); + bool any_auto_config = false; + + for (size_t i = 0; i < m_messages.size(); ++i) + { + message &m = m_messages[i]; + if ((m.type == message_type::auto_config_data) && (m.state == message_state::waiting)) + { + if (auto_config_messages[m.signer_index] == 0) + { + auto_config_messages[m.signer_index] = m.id; + any_auto_config = true; + } + // else duplicate auto config data, ignore + } + } + + if (any_auto_config) + { + bool auto_config_complete = message_ids_complete(auto_config_messages); + if (auto_config_complete) + { + processing_data data; + data.processing = message_processing::process_auto_config_data; + data.message_ids = auto_config_messages; + data.message_ids.erase(data.message_ids.begin()); + data_list.push_back(data); + return true; + } + else + { + wait_reason = tr("Auto-config cannot proceed because auto config data from other signers is not complete"); + return false; + // With ANY auto config data present but not complete refuse to check for any + // other processing. Manually delete those messages to abort such an auto config + // phase if needed. + } + } + + // Any signer config that arrived will be processed right away, regardless of other things that may wait + for (size_t i = 0; i < m_messages.size(); ++i) + { + message &m = m_messages[i]; + if ((m.type == message_type::signer_config) && (m.state == message_state::waiting)) + { + processing_data data; + data.processing = message_processing::process_signer_config; + data.message_ids.push_back(m.id); + data_list.push_back(data); + return true; + } + } + + // ALL of the following processings depend on the signer info being complete + if (!signer_config_complete()) + { + wait_reason = tr("The signer config is not complete."); + return false; + } + + if (!state.multisig) + { + + if (!any_message_of_type(message_type::key_set, message_direction::out)) + { + // With the own key set not yet ready we must do "prepare_multisig" first; + // Key sets from other signers may be here already, but if we process them now + // the wallet will go multisig too early: we can't produce our own key set any more! + processing_data data; + data.processing = message_processing::prepare_multisig; + data_list.push_back(data); + return true; + } + + // Ids of key set messages per signer index, to check completeness + // Naturally, does not care about the order of the messages and is trivial to secure against + // key sets that were received more than once + // With full M/N multisig now possible consider only key sets of the right round, i.e. + // with not yet multisig the only possible round 0 + std::vector<uint32_t> key_set_messages(m_num_authorized_signers, 0); + + for (size_t i = 0; i < m_messages.size(); ++i) + { + message &m = m_messages[i]; + if ((m.type == message_type::key_set) && (m.state == message_state::waiting) + && (m.round == 0)) + { + if (key_set_messages[m.signer_index] == 0) + { + key_set_messages[m.signer_index] = m.id; + } + // else duplicate key set, ignore + } + } + + bool key_sets_complete = message_ids_complete(key_set_messages); + if (key_sets_complete) + { + // Nothing else can be ready to process earlier than this, ignore everything else and give back + processing_data data; + data.processing = message_processing::make_multisig; + data.message_ids = key_set_messages; + data.message_ids.erase(data.message_ids.begin()); + data_list.push_back(data); + return true; + } + else + { + wait_reason = tr("Wallet can't go multisig because key sets from other signers are missing or not complete."); + return false; + } + } + + if (state.multisig && !state.multisig_is_ready) + { + // In the case of M/N multisig the call 'wallet2::multisig' returns already true + // after "make_multisig" but with calls to "exchange_multisig_keys" still needed, and + // sets the parameter 'ready' to false to document this particular "in-between" state. + // So what may be possible here, with all necessary messages present, is a call to + // "exchange_multisig_keys". + // Consider only messages belonging to the next round to do, which has the number + // "state.multisig_rounds_passed". + std::vector<uint32_t> additional_key_set_messages(m_num_authorized_signers, 0); + + for (size_t i = 0; i < m_messages.size(); ++i) + { + message &m = m_messages[i]; + if ((m.type == message_type::additional_key_set) && (m.state == message_state::waiting) + && (m.round == state.multisig_rounds_passed)) + { + if (additional_key_set_messages[m.signer_index] == 0) + { + additional_key_set_messages[m.signer_index] = m.id; + } + // else duplicate key set, ignore + } + } + + bool key_sets_complete = message_ids_complete(additional_key_set_messages); + if (key_sets_complete) + { + processing_data data; + data.processing = message_processing::exchange_multisig_keys; + data.message_ids = additional_key_set_messages; + data.message_ids.erase(data.message_ids.begin()); + data_list.push_back(data); + return true; + } + else + { + wait_reason = tr("Wallet can't start another key exchange round because key sets from other signers are missing or not complete."); + return false; + } + } + + // Properly exchanging multisig sync data is easiest and most transparent + // for the user if a wallet sends its own data first and processes any received + // sync data afterwards so that's the order that the MMS enforces here. + // (Technically, it seems to work also the other way round.) + // + // To check whether a NEW round of syncing is necessary the MMS works with a + // "wallet state": new state means new syncing needed. + // + // The MMS monitors the "wallet state" by recording "wallet heights" as + // numbers of transfers present in a wallet at the time of message creation. While + // not watertight, this quite simple scheme should already suffice to trigger + // and orchestrate a sensible exchange of sync data. + if (state.has_multisig_partial_key_images || force_sync) + { + // Sync is necessary and not yet completed: Processing of transactions + // will only be possible again once properly synced + // Check first whether we generated already OUR sync info; take note of + // any processable sync info from other signers on the way in case we need it + bool own_sync_data_created = false; + std::vector<uint32_t> sync_messages(m_num_authorized_signers, 0); + for (size_t i = 0; i < m_messages.size(); ++i) + { + message &m = m_messages[i]; + if ((m.type == message_type::multisig_sync_data) && (force_sync || (m.wallet_height == wallet_height))) + // It's data for the same "round" of syncing, on the same "wallet height", therefore relevant + // With "force_sync" take ANY waiting sync data, maybe it will work out + { + if (m.direction == message_direction::out) + { + own_sync_data_created = true; + // Ignore whether sent already or not, and assume as complete if several other signers there + } + else if ((m.direction == message_direction::in) && (m.state == message_state::waiting)) + { + if (sync_messages[m.signer_index] == 0) + { + sync_messages[m.signer_index] = m.id; + } + // else duplicate sync message, ignore + } + } + } + if (!own_sync_data_created) + { + // As explained above, creating sync data BEFORE processing such data from + // other signers reliably works, so insist on that here + processing_data data; + data.processing = message_processing::create_sync_data; + data_list.push_back(data); + return true; + } + uint32_t id_count = (uint32_t)get_other_signers_id_count(sync_messages); + // Do we have sync data from ALL other signers? + bool all_sync_data = id_count == (m_num_authorized_signers - 1); + // Do we have just ENOUGH sync data to have a minimal viable sync set? + // In cases like 2/3 multisig we don't need messages from ALL other signers, only + // from enough of them i.e. num_required_signers minus 1 messages + bool enough_sync_data = id_count >= (m_num_required_signers - 1); + bool sync = false; + wait_reason = tr("Syncing not done because multisig sync data from other signers are missing or not complete."); + if (all_sync_data) + { + sync = true; + } + else if (enough_sync_data) + { + if (force_sync) + { + sync = true; + } + else + { + // Don't sync, but give a hint how this minimal set COULD be synced if really wanted + wait_reason += (boost::format("\nUse \"mms next sync\" if you want to sync with just %s out of %s authorized signers and transact just with them") + % (m_num_required_signers - 1) % (m_num_authorized_signers - 1)).str(); + } + } + if (sync) + { + processing_data data; + data.processing = message_processing::process_sync_data; + for (size_t i = 0; i < sync_messages.size(); ++i) + { + uint32_t id = sync_messages[i]; + if (id != 0) + { + data.message_ids.push_back(id); + } + } + data_list.push_back(data); + return true; + } + else + { + // We can't proceed to any transactions until we have synced; "wait_reason" already set above + return false; + } + } + + bool waiting_found = false; + bool note_found = false; + bool sync_data_found = false; + for (size_t i = 0; i < m_messages.size(); ++i) + { + message &m = m_messages[i]; + if (m.state == message_state::waiting) + { + waiting_found = true; + switch (m.type) + { + case message_type::fully_signed_tx: + { + // We can either submit it ourselves, or send it to any other signer for submission + processing_data data; + data.processing = message_processing::submit_tx; + data.message_ids.push_back(m.id); + data_list.push_back(data); + + data.processing = message_processing::send_tx; + for (uint32_t j = 1; j < m_num_authorized_signers; ++j) + { + data.receiving_signer_index = j; + data_list.push_back(data); + } + return true; + } + + case message_type::partially_signed_tx: + { + if (m.signer_index == 0) + { + // We started this ourselves, or signed it but with still signatures missing: + // We can send it to any other signer for signing / further signing + // In principle it does not make sense to send it back to somebody who + // already signed, but the MMS does not / not yet keep track of that, + // because that would be somewhat complicated. + processing_data data; + data.processing = message_processing::send_tx; + data.message_ids.push_back(m.id); + for (uint32_t j = 1; j < m_num_authorized_signers; ++j) + { + data.receiving_signer_index = j; + data_list.push_back(data); + } + return true; + } + else + { + // Somebody else sent this to us: We can sign it + // It would be possible to just pass it on, but that's not directly supported here + processing_data data; + data.processing = message_processing::sign_tx; + data.message_ids.push_back(m.id); + data_list.push_back(data); + return true; + } + } + + case message_type::note: + note_found = true; + break; + + case message_type::multisig_sync_data: + sync_data_found = true; + break; + + default: + break; + } + } + } + if (waiting_found) + { + wait_reason = tr("There are waiting messages, but nothing is ready to process under normal circumstances"); + if (sync_data_found) + { + wait_reason += tr("\nUse \"mms next sync\" if you want to force processing of the waiting sync data"); + } + if (note_found) + { + wait_reason += tr("\nUse \"mms note\" to display the waiting notes"); + } + } + else + { + wait_reason = tr("There are no messages waiting to be processed."); + } + + return false; +} + +void message_store::set_messages_processed(const processing_data &data) +{ + for (size_t i = 0; i < data.message_ids.size(); ++i) + { + set_message_processed_or_sent(data.message_ids[i]); + } +} + +void message_store::set_message_processed_or_sent(uint32_t id) +{ + message &m = get_message_ref_by_id(id); + if (m.state == message_state::waiting) + { + // So far a fairly cautious and conservative strategy: Only delete from Bitmessage + // when fully processed (and e.g. not already after reception and writing into + // the message store file) + delete_transport_message(id); + m.state = message_state::processed; + } + else if (m.state == message_state::ready_to_send) + { + m.state = message_state::sent; + } + m.modified = (uint64_t)time(NULL); +} + +void message_store::encrypt(crypto::public_key public_key, const std::string &plaintext, + std::string &ciphertext, crypto::public_key &encryption_public_key, crypto::chacha_iv &iv) +{ + crypto::secret_key encryption_secret_key; + crypto::generate_keys(encryption_public_key, encryption_secret_key); + + crypto::key_derivation derivation; + bool success = crypto::generate_key_derivation(public_key, encryption_secret_key, derivation); + THROW_WALLET_EXCEPTION_IF(!success, tools::error::wallet_internal_error, "Failed to generate key derivation for message encryption"); + + crypto::chacha_key chacha_key; + crypto::generate_chacha_key(&derivation, sizeof(derivation), chacha_key, 1); + iv = crypto::rand<crypto::chacha_iv>(); + ciphertext.resize(plaintext.size()); + crypto::chacha20(plaintext.data(), plaintext.size(), chacha_key, iv, &ciphertext[0]); +} + +void message_store::decrypt(const std::string &ciphertext, const crypto::public_key &encryption_public_key, const crypto::chacha_iv &iv, + const crypto::secret_key &view_secret_key, std::string &plaintext) +{ + crypto::key_derivation derivation; + bool success = crypto::generate_key_derivation(encryption_public_key, view_secret_key, derivation); + THROW_WALLET_EXCEPTION_IF(!success, tools::error::wallet_internal_error, "Failed to generate key derivation for message decryption"); + crypto::chacha_key chacha_key; + crypto::generate_chacha_key(&derivation, sizeof(derivation), chacha_key, 1); + plaintext.resize(ciphertext.size()); + crypto::chacha20(ciphertext.data(), ciphertext.size(), chacha_key, iv, &plaintext[0]); +} + +void message_store::send_message(const multisig_wallet_state &state, uint32_t id) +{ + message &m = get_message_ref_by_id(id); + const authorized_signer &me = m_signers[0]; + const authorized_signer &receiver = m_signers[m.signer_index]; + transport_message dm; + crypto::public_key public_key; + + dm.timestamp = (uint64_t)time(NULL); + dm.subject = "MMS V0 " + tools::get_human_readable_timestamp(dm.timestamp); + dm.source_transport_address = me.transport_address; + dm.source_monero_address = me.monero_address; + if (m.type == message_type::auto_config_data) + { + // Encrypt with the public key derived from the auto-config token, and send to the + // transport address likewise derived from that token + public_key = me.auto_config_public_key; + dm.destination_transport_address = me.auto_config_transport_address; + // The destination Monero address is not yet known + memset(&dm.destination_monero_address, 0, sizeof(cryptonote::account_public_address)); + } + else + { + // Encrypt with the receiver's view public key + public_key = receiver.monero_address.m_view_public_key; + const authorized_signer &receiver = m_signers[m.signer_index]; + dm.destination_monero_address = receiver.monero_address; + dm.destination_transport_address = receiver.transport_address; + } + encrypt(public_key, m.content, dm.content, dm.encryption_public_key, dm.iv); + dm.type = (uint32_t)m.type; + dm.hash = crypto::cn_fast_hash(dm.content.data(), dm.content.size()); + dm.round = m.round; + + crypto::generate_signature(dm.hash, me.monero_address.m_view_public_key, state.view_secret_key, dm.signature); + + m_transporter.send_message(dm); + + m.state=message_state::sent; + m.sent= (uint64_t)time(NULL); +} + +bool message_store::check_for_messages(const multisig_wallet_state &state, std::vector<message> &messages) +{ + m_run.store(true, std::memory_order_relaxed); + const authorized_signer &me = m_signers[0]; + std::vector<std::string> destinations; + destinations.push_back(me.transport_address); + for (uint32_t i = 1; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = m_signers[i]; + if (m.auto_config_running) + { + destinations.push_back(m.auto_config_transport_address); + } + } + std::vector<transport_message> transport_messages; + bool r = m_transporter.receive_messages(destinations, transport_messages); + if (!m_run.load(std::memory_order_relaxed)) + { + // Stop was called, don't waste time processing the messages + // (but once started processing them, don't react to stop request anymore, avoid receiving them "partially)" + return false; + } + + bool new_messages = false; + for (size_t i = 0; i < transport_messages.size(); ++i) + { + transport_message &rm = transport_messages[i]; + if (any_message_with_hash(rm.hash)) + { + // Already seen, do not take again + } + else + { + uint32_t sender_index; + bool take = false; + message_type type = static_cast<message_type>(rm.type); + crypto::secret_key decrypt_key = state.view_secret_key; + if (type == message_type::auto_config_data) + { + // Find out which signer sent it by checking which auto config transport address + // the message was sent to + for (uint32_t i = 1; i < m_num_authorized_signers; ++i) + { + const authorized_signer &m = m_signers[i]; + if (m.auto_config_transport_address == rm.destination_transport_address) + { + take = true; + sender_index = i; + decrypt_key = m.auto_config_secret_key; + break; + } + } + } + else if (type == message_type::signer_config) + { + // Typically we can't check yet whether we know the sender, so take from any + // and pretend it's from "me" because we might have nothing else yet + take = true; + sender_index = 0; + } + else + { + // Only accept from senders that are known as signer here, otherwise just ignore + take = get_signer_index_by_monero_address(rm.source_monero_address, sender_index); + } + if (take && (type != message_type::auto_config_data)) + { + // If the destination address is known, check it as well; this additional filter + // allows using the same transport address for multiple signers + take = rm.destination_monero_address == me.monero_address; + } + if (take) + { + crypto::hash actual_hash = crypto::cn_fast_hash(rm.content.data(), rm.content.size()); + THROW_WALLET_EXCEPTION_IF(actual_hash != rm.hash, tools::error::wallet_internal_error, "Message hash mismatch"); + + bool signature_valid = crypto::check_signature(actual_hash, rm.source_monero_address.m_view_public_key, rm.signature); + THROW_WALLET_EXCEPTION_IF(!signature_valid, tools::error::wallet_internal_error, "Message signature not valid"); + + std::string plaintext; + decrypt(rm.content, rm.encryption_public_key, rm.iv, decrypt_key, plaintext); + size_t index = add_message(state, sender_index, (message_type)rm.type, message_direction::in, plaintext); + message &m = m_messages[index]; + m.hash = rm.hash; + m.transport_id = rm.transport_id; + m.sent = rm.timestamp; + m.round = rm.round; + m.signature_count = rm.signature_count; + messages.push_back(m); + new_messages = true; + } + } + } + return new_messages; +} + +void message_store::delete_transport_message(uint32_t id) +{ + const message &m = get_message_by_id(id); + if (!m.transport_id.empty()) + { + m_transporter.delete_message(m.transport_id); + } +} + +std::string message_store::account_address_to_string(const cryptonote::account_public_address &account_address) const +{ + return get_account_address_as_str(m_nettype, false, account_address); +} + +const char* message_store::message_type_to_string(message_type type) +{ + switch (type) + { + case message_type::key_set: + return tr("key set"); + case message_type::additional_key_set: + return tr("additional key set"); + case message_type::multisig_sync_data: + return tr("multisig sync data"); + case message_type::partially_signed_tx: + return tr("partially signed tx"); + case message_type::fully_signed_tx: + return tr("fully signed tx"); + case message_type::note: + return tr("note"); + case message_type::signer_config: + return tr("signer config"); + case message_type::auto_config_data: + return tr("auto-config data"); + default: + return tr("unknown message type"); + } +} + +const char* message_store::message_direction_to_string(message_direction direction) +{ + switch (direction) + { + case message_direction::in: + return tr("in"); + case message_direction::out: + return tr("out"); + default: + return tr("unknown message direction"); + } +} + +const char* message_store::message_state_to_string(message_state state) +{ + switch (state) + { + case message_state::ready_to_send: + return tr("ready to send"); + case message_state::sent: + return tr("sent"); + case message_state::waiting: + return tr("waiting"); + case message_state::processed: + return tr("processed"); + case message_state::cancelled: + return tr("cancelled"); + default: + return tr("unknown message state"); + } +} + +// Convert a signer to string suitable for a column in a list, with 'max_width' +// Format: label: transport_address +std::string message_store::signer_to_string(const authorized_signer &signer, uint32_t max_width) +{ + std::string s = ""; + s.reserve(max_width); + uint32_t avail = max_width; + uint32_t label_len = signer.label.length(); + if (label_len > avail) + { + s.append(signer.label.substr(0, avail - 2)); + s.append(".."); + return s; + } + s.append(signer.label); + avail -= label_len; + uint32_t transport_addr_len = signer.transport_address.length(); + if ((transport_addr_len > 0) && (avail > 10)) + { + s.append(": "); + avail -= 2; + if (transport_addr_len <= avail) + { + s.append(signer.transport_address); + } + else + { + s.append(signer.transport_address.substr(0, avail-2)); + s.append(".."); + } + } + return s; +} + +} diff --git a/src/wallet/message_store.h b/src/wallet/message_store.h new file mode 100644 index 000000000..7d26f7889 --- /dev/null +++ b/src/wallet/message_store.h @@ -0,0 +1,420 @@ +// Copyright (c) 2018, 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 <cstdlib> +#include <string> +#include <vector> +#include "crypto/hash.h" +#include <boost/serialization/vector.hpp> +#include <boost/program_options/variables_map.hpp> +#include <boost/program_options/options_description.hpp> +#include <boost/optional/optional.hpp> +#include "serialization/serialization.h" +#include "cryptonote_basic/cryptonote_boost_serialization.h" +#include "cryptonote_basic/account_boost_serialization.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "common/i18n.h" +#include "common/command_line.h" +#include "wipeable_string.h" +#include "message_transporter.h" + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.mms" +#define AUTO_CONFIG_TOKEN_BYTES 4 +#define AUTO_CONFIG_TOKEN_PREFIX "mms" + +namespace mms +{ + enum class message_type + { + key_set, + additional_key_set, + multisig_sync_data, + partially_signed_tx, + fully_signed_tx, + note, + signer_config, + auto_config_data + }; + + enum class message_direction + { + in, + out + }; + + enum class message_state + { + ready_to_send, + sent, + + waiting, + processed, + + cancelled + }; + + enum class message_processing + { + prepare_multisig, + make_multisig, + exchange_multisig_keys, + create_sync_data, + process_sync_data, + sign_tx, + send_tx, + submit_tx, + process_signer_config, + process_auto_config_data + }; + + struct message + { + uint32_t id; + message_type type; + message_direction direction; + std::string content; + uint64_t created; + uint64_t modified; + uint64_t sent; + uint32_t signer_index; + crypto::hash hash; + message_state state; + uint32_t wallet_height; + uint32_t round; + uint32_t signature_count; + std::string transport_id; + }; + // "wallet_height" (for lack of a short name that would describe what it is about) + // is the number of transfers present in the wallet at the time of message + // construction; used to coordinate generation of sync info (which depends + // on the content of the wallet at time of generation) + + struct authorized_signer + { + std::string label; + std::string transport_address; + bool monero_address_known; + cryptonote::account_public_address monero_address; + bool me; + uint32_t index; + std::string auto_config_token; + crypto::public_key auto_config_public_key; + crypto::secret_key auto_config_secret_key; + std::string auto_config_transport_address; + bool auto_config_running; + + authorized_signer() + { + monero_address_known = false; + memset(&monero_address, 0, sizeof(cryptonote::account_public_address)); + index = 0; + auto_config_public_key = crypto::null_pkey; + auto_config_secret_key = crypto::null_skey; + auto_config_running = false; + }; + }; + + struct processing_data + { + message_processing processing; + std::vector<uint32_t> message_ids; + uint32_t receiving_signer_index = 0; + }; + + struct file_transport_message + { + cryptonote::account_public_address sender_address; + crypto::chacha_iv iv; + crypto::public_key encryption_public_key; + message internal_message; + }; + + struct auto_config_data + { + std::string label; + std::string transport_address; + cryptonote::account_public_address monero_address; + }; + + // Overal .mms file structure, with the "message_store" object serialized to and + // encrypted in "encrypted_data" + struct file_data + { + std::string magic_string; + uint32_t file_version; + crypto::chacha_iv iv; + std::string encrypted_data; + }; + + // The following struct provides info about the current state of a "wallet2" object + // at the time of a "message_store" method call that those methods need. See on the + // one hand a first parameter of this type for several of those methods, and on the + // other hand the method "wallet2::get_multisig_wallet_state" which clients like the + // CLI wallet can use to get that info. + // + // Note that in the case of a wallet that is already multisig "address" is NOT the + // multisig address, but the "original" wallet address at creation time. Likewise + // "view_secret_key" is the original view secret key then. + // + // This struct definition is here and not in "wallet2.h" to avoid circular imports. + struct multisig_wallet_state + { + cryptonote::account_public_address address; + cryptonote::network_type nettype; + crypto::secret_key view_secret_key; + bool multisig; + bool multisig_is_ready; + bool has_multisig_partial_key_images; + uint32_t multisig_rounds_passed; + size_t num_transfer_details; + std::string mms_file; + }; + + class message_store + { + public: + message_store(); + // Initialize and start to use the MMS, set the first signer, this wallet itself + // Filename, if not null and not empty, is used to create the ".mms" file + // reset it if already used, with deletion of all signers and messages + void init(const multisig_wallet_state &state, const std::string &own_label, + const std::string &own_transport_address, uint32_t num_authorized_signers, uint32_t num_required_signers); + void set_active(bool active) { m_active = active; }; + void set_auto_send(bool auto_send) { m_auto_send = auto_send; }; + void set_options(const boost::program_options::variables_map& vm); + void set_options(const std::string &bitmessage_address, const epee::wipeable_string &bitmessage_login); + bool get_active() const { return m_active; }; + bool get_auto_send() const { return m_auto_send; }; + uint32_t get_num_required_signers() const { return m_num_required_signers; }; + uint32_t get_num_authorized_signers() const { return m_num_authorized_signers; }; + + void set_signer(const multisig_wallet_state &state, + uint32_t index, + const boost::optional<std::string> &label, + const boost::optional<std::string> &transport_address, + const boost::optional<cryptonote::account_public_address> monero_address); + + const authorized_signer &get_signer(uint32_t index) const; + bool get_signer_index_by_monero_address(const cryptonote::account_public_address &monero_address, uint32_t &index) const; + bool get_signer_index_by_label(const std::string label, uint32_t &index) const; + const std::vector<authorized_signer> &get_all_signers() const { return m_signers; }; + bool signer_config_complete() const; + bool signer_labels_complete() const; + void get_signer_config(std::string &signer_config); + void unpack_signer_config(const multisig_wallet_state &state, const std::string &signer_config, + std::vector<authorized_signer> &signers); + void process_signer_config(const multisig_wallet_state &state, const std::string &signer_config); + + void start_auto_config(const multisig_wallet_state &state); + bool check_auto_config_token(const std::string &raw_token, + std::string &adjusted_token) const; + size_t add_auto_config_data_message(const multisig_wallet_state &state, + const std::string &auto_config_token); + void process_auto_config_data_message(uint32_t id); + void stop_auto_config(); + + // Process data just created by "me" i.e. the own local wallet, e.g. as the result of a "prepare_multisig" command + // Creates the resulting messages to the right signers + void process_wallet_created_data(const multisig_wallet_state &state, message_type type, const std::string &content); + + // Go through all the messages, look at the "ready to process" ones, and check whether any single one + // or any group of them can be processed, because they are processable as single messages (like a tx + // that is fully signed and thus ready for submit to the net) or because they form a complete group + // (e.g. key sets from all authorized signers to make the wallet multisig). If there are multiple + // candidates, e.g. in 2/3 multisig sending to one OR the other signer to sign, there will be more + // than 1 element in 'data' for the user to choose. If nothing is ready "false" is returned. + // The method mostly ignores the order in which the messages were received because messages may be delayed + // (e.g. sync data from a signer arrives AFTER a transaction to submit) or because message time stamps + // may be wrong so it's not possible to order them reliably. + // Messages also may be ready by themselves but the wallet not yet ready for them (e.g. sync data already + // arriving when the wallet is not yet multisig because key sets were delayed or were lost altogether.) + // If nothing is ready 'wait_reason' may contain further info about the reason why. + bool get_processable_messages(const multisig_wallet_state &state, + bool force_sync, + std::vector<processing_data> &data_list, + std::string &wait_reason); + void set_messages_processed(const processing_data &data); + + size_t add_message(const multisig_wallet_state &state, + uint32_t signer_index, message_type type, message_direction direction, + const std::string &content); + const std::vector<message> &get_all_messages() const { return m_messages; }; + bool get_message_by_id(uint32_t id, message &m) const; + message get_message_by_id(uint32_t id) const; + void set_message_processed_or_sent(uint32_t id); + void delete_message(uint32_t id); + void delete_all_messages(); + void get_sanitized_message_text(const message &m, std::string &sanitized_text) const; + + void send_message(const multisig_wallet_state &state, uint32_t id); + bool check_for_messages(const multisig_wallet_state &state, std::vector<message> &messages); + void stop() { m_run.store(false, std::memory_order_relaxed); m_transporter.stop(); } + + void write_to_file(const multisig_wallet_state &state, const std::string &filename); + void read_from_file(const multisig_wallet_state &state, const std::string &filename); + + template <class t_archive> + inline void serialize(t_archive &a, const unsigned int ver) + { + a & m_active; + a & m_num_authorized_signers; + a & m_nettype; + a & m_num_required_signers; + a & m_signers; + a & m_messages; + a & m_next_message_id; + a & m_auto_send; + } + + static const char* message_type_to_string(message_type type); + static const char* message_direction_to_string(message_direction direction); + static const char* message_state_to_string(message_state state); + std::string signer_to_string(const authorized_signer &signer, uint32_t max_width); + + static const char *tr(const char *str) { return i18n_translate(str, "tools::mms"); } + static void init_options(boost::program_options::options_description& desc_params); + + private: + bool m_active; + uint32_t m_num_authorized_signers; + uint32_t m_num_required_signers; + bool m_auto_send; + cryptonote::network_type m_nettype; + std::vector<authorized_signer> m_signers; + std::vector<message> m_messages; + uint32_t m_next_message_id; + std::string m_filename; + message_transporter m_transporter; + std::atomic<bool> m_run; + + bool get_message_index_by_id(uint32_t id, size_t &index) const; + size_t get_message_index_by_id(uint32_t id) const; + message& get_message_ref_by_id(uint32_t id); + bool any_message_of_type(message_type type, message_direction direction) const; + bool any_message_with_hash(const crypto::hash &hash) const; + size_t get_other_signers_id_count(const std::vector<uint32_t> &ids) const; + bool message_ids_complete(const std::vector<uint32_t> &ids) const; + void encrypt(crypto::public_key public_key, const std::string &plaintext, + std::string &ciphertext, crypto::public_key &encryption_public_key, crypto::chacha_iv &iv); + void decrypt(const std::string &ciphertext, const crypto::public_key &encryption_public_key, const crypto::chacha_iv &iv, + const crypto::secret_key &view_secret_key, std::string &plaintext); + std::string create_auto_config_token(); + void setup_signer_for_auto_config(uint32_t index, const std::string token, bool receiving); + void delete_transport_message(uint32_t id); + std::string account_address_to_string(const cryptonote::account_public_address &account_address) const; + void save(const multisig_wallet_state &state); + }; +} + +BOOST_CLASS_VERSION(mms::file_data, 0) +BOOST_CLASS_VERSION(mms::message_store, 0) +BOOST_CLASS_VERSION(mms::message, 0) +BOOST_CLASS_VERSION(mms::file_transport_message, 0) +BOOST_CLASS_VERSION(mms::authorized_signer, 1) +BOOST_CLASS_VERSION(mms::auto_config_data, 0) + +namespace boost +{ + namespace serialization + { + template <class Archive> + inline void serialize(Archive &a, mms::file_data &x, const boost::serialization::version_type ver) + { + a & x.magic_string; + a & x.file_version; + a & x.iv; + a & x.encrypted_data; + } + + template <class Archive> + inline void serialize(Archive &a, mms::message &x, const boost::serialization::version_type ver) + { + a & x.id; + a & x.type; + a & x.direction; + a & x.content; + a & x.created; + a & x.modified; + a & x.sent; + a & x.signer_index; + a & x.hash; + a & x.state; + a & x.wallet_height; + a & x.round; + a & x.signature_count; + a & x.transport_id; + } + + template <class Archive> + inline void serialize(Archive &a, mms::authorized_signer &x, const boost::serialization::version_type ver) + { + a & x.label; + a & x.transport_address; + a & x.monero_address_known; + a & x.monero_address; + a & x.me; + a & x.index; + if (ver < 1) + { + return; + } + a & x.auto_config_token; + a & x.auto_config_public_key; + a & x.auto_config_secret_key; + a & x.auto_config_transport_address; + a & x.auto_config_running; + } + + template <class Archive> + inline void serialize(Archive &a, mms::auto_config_data &x, const boost::serialization::version_type ver) + { + a & x.label; + a & x.transport_address; + a & x.monero_address; + } + + template <class Archive> + inline void serialize(Archive &a, mms::file_transport_message &x, const boost::serialization::version_type ver) + { + a & x.sender_address; + a & x.iv; + a & x.encryption_public_key; + a & x.internal_message; + } + + template <class Archive> + inline void serialize(Archive &a, crypto::chacha_iv &x, const boost::serialization::version_type ver) + { + a & x.data; + } + + } +} diff --git a/src/wallet/message_transporter.cpp b/src/wallet/message_transporter.cpp new file mode 100644 index 000000000..eafd13d3b --- /dev/null +++ b/src/wallet/message_transporter.cpp @@ -0,0 +1,317 @@ +// Copyright (c) 2018, 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 "message_transporter.h" +#include "string_coding.h" +#include <boost/format.hpp> +#include "wallet_errors.h" +#include "net/http_client.h" +#include "net/net_parse_helpers.h" +#include <algorithm> + +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "wallet.mms" +#define PYBITMESSAGE_DEFAULT_API_PORT 8442 + +namespace mms +{ + +namespace bitmessage_rpc +{ + + struct message_info + { + uint32_t encodingType; + std::string toAddress; + uint32_t read; + std::string msgid; + std::string message; + std::string fromAddress; + std::string receivedTime; + std::string subject; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(encodingType) + KV_SERIALIZE(toAddress) + KV_SERIALIZE(read) + KV_SERIALIZE(msgid) + KV_SERIALIZE(message); + KV_SERIALIZE(fromAddress) + KV_SERIALIZE(receivedTime) + KV_SERIALIZE(subject) + END_KV_SERIALIZE_MAP() + }; + + struct inbox_messages_response + { + std::vector<message_info> inboxMessages; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(inboxMessages) + END_KV_SERIALIZE_MAP() + }; + +} + +message_transporter::message_transporter() +{ + m_run = true; +} + +void message_transporter::set_options(const std::string &bitmessage_address, const epee::wipeable_string &bitmessage_login) +{ + m_bitmessage_url = bitmessage_address; + epee::net_utils::http::url_content address_parts{}; + epee::net_utils::parse_url(m_bitmessage_url, address_parts); + if (address_parts.port == 0) + { + address_parts.port = PYBITMESSAGE_DEFAULT_API_PORT; + } + m_bitmessage_login = bitmessage_login; + + m_http_client.set_server(address_parts.host, std::to_string(address_parts.port), boost::none); +} + +bool message_transporter::receive_messages(const std::vector<std::string> &destination_transport_addresses, + std::vector<transport_message> &messages) +{ + // The message body of the Bitmessage message is basically the transport message, as JSON (and nothing more). + // Weeding out other, non-MMS messages is done in a simple way: If it deserializes without error, it's an MMS message + // That JSON is Base64-encoded by the MMS because the Monero epee JSON serializer does not escape anything and happily + // includes even 0 (NUL) in strings, which might confuse Bitmessage or at least display confusingly in the client. + // There is yet another Base64-encoding of course as part of the Bitmessage API for the message body parameter + // The Bitmessage API call "getAllInboxMessages" gives back a JSON array with all the messages (despite using + // XML-RPC for the calls, and not JSON-RPC ...) + m_run.store(true, std::memory_order_relaxed); + std::string request; + start_xml_rpc_cmd(request, "getAllInboxMessages"); + end_xml_rpc_cmd(request); + std::string answer; + post_request(request, answer); + + std::string json = get_str_between_tags(answer, "<string>", "</string>"); + bitmessage_rpc::inbox_messages_response bitmessage_res; + epee::serialization::load_t_from_json(bitmessage_res, json); + size_t size = bitmessage_res.inboxMessages.size(); + messages.clear(); + + for (size_t i = 0; i < size; ++i) + { + if (!m_run.load(std::memory_order_relaxed)) + { + // Stop was called, don't waste time processing any more messages + return false; + } + const bitmessage_rpc::message_info &message_info = bitmessage_res.inboxMessages[i]; + if (std::find(destination_transport_addresses.begin(), destination_transport_addresses.end(), message_info.toAddress) != destination_transport_addresses.end()) + { + transport_message message; + bool is_mms_message = false; + try + { + // First Base64-decoding: The message body is Base64 in the Bitmessage API + std::string message_body = epee::string_encoding::base64_decode(message_info.message); + // Second Base64-decoding: The MMS uses Base64 to hide non-textual data in its JSON from Bitmessage + json = epee::string_encoding::base64_decode(message_body); + epee::serialization::load_t_from_json(message, json); + is_mms_message = true; + } + catch(const std::exception& e) + { + } + if (is_mms_message) + { + message.transport_id = message_info.msgid; + messages.push_back(message); + } + } + } + + return true; +} + +bool message_transporter::send_message(const transport_message &message) +{ + // <toAddress> <fromAddress> <subject> <message> [encodingType [TTL]] + std::string request; + start_xml_rpc_cmd(request, "sendMessage"); + add_xml_rpc_string_param(request, message.destination_transport_address); + add_xml_rpc_string_param(request, message.source_transport_address); + add_xml_rpc_base64_param(request, message.subject); + std::string json = epee::serialization::store_t_to_json(message); + std::string message_body = epee::string_encoding::base64_encode(json); // See comment in "receive_message" about reason for (double-)Base64 encoding + add_xml_rpc_base64_param(request, message_body); + add_xml_rpc_integer_param(request, 2); + end_xml_rpc_cmd(request); + std::string answer; + post_request(request, answer); + return true; +} + +bool message_transporter::delete_message(const std::string &transport_id) +{ + std::string request; + start_xml_rpc_cmd(request, "trashMessage"); + add_xml_rpc_string_param(request, transport_id); + end_xml_rpc_cmd(request); + std::string answer; + post_request(request, answer); + return true; +} + +// Deterministically derive a transport / Bitmessage address from 'seed' (the 10-hex-digits +// auto-config token will be used), but do not set it up for receiving in PyBitmessage as +// well, because it's possible the address will only ever be used to SEND auto-config data +std::string message_transporter::derive_transport_address(const std::string &seed) +{ + std::string request; + start_xml_rpc_cmd(request, "getDeterministicAddress"); + add_xml_rpc_base64_param(request, seed); + add_xml_rpc_integer_param(request, 4); // addressVersionNumber + add_xml_rpc_integer_param(request, 1); // streamNumber + end_xml_rpc_cmd(request); + std::string answer; + post_request(request, answer); + std::string address = get_str_between_tags(answer, "<string>", "</string>"); + return address; +} + +// Derive a transport address and configure it for receiving in PyBitmessage, typically +// for receiving auto-config messages by the wallet of the auto-config organizer +std::string message_transporter::derive_and_receive_transport_address(const std::string &seed) +{ + // We need to call both "get_deterministic_address" AND "createDeterministicAddresses" + // because we won't get back the address from the latter call if it exists already + std::string address = derive_transport_address(seed); + + std::string request; + start_xml_rpc_cmd(request, "createDeterministicAddresses"); + add_xml_rpc_base64_param(request, seed); + add_xml_rpc_integer_param(request, 1); // numberOfAddresses + add_xml_rpc_integer_param(request, 4); // addressVersionNumber + end_xml_rpc_cmd(request); + std::string answer; + post_request(request, answer); + + return address; +} + +bool message_transporter::delete_transport_address(const std::string &transport_address) +{ + std::string request; + start_xml_rpc_cmd(request, "deleteAddress"); + add_xml_rpc_string_param(request, transport_address); + end_xml_rpc_cmd(request); + std::string answer; + return post_request(request, answer); +} + +bool message_transporter::post_request(const std::string &request, std::string &answer) +{ + // Somehow things do not work out if one tries to connect "m_http_client" to Bitmessage + // and keep it connected over the course of several calls. But with a new connection per + // call and disconnecting after the call there is no problem (despite perhaps a small + // slowdown) + epee::net_utils::http::fields_list additional_params; + + // Basic access authentication according to RFC 7617 (which the epee HTTP classes do not seem to support?) + // "m_bitmessage_login" just contains what is needed here, "user:password" + std::string auth_string = epee::string_encoding::base64_encode((const unsigned char*)m_bitmessage_login.data(), m_bitmessage_login.size()); + auth_string.insert(0, "Basic "); + additional_params.push_back(std::make_pair("Authorization", auth_string)); + + additional_params.push_back(std::make_pair("Content-Type", "application/xml; charset=utf-8")); + const epee::net_utils::http::http_response_info* response = NULL; + std::chrono::milliseconds timeout = std::chrono::seconds(15); + bool r = m_http_client.invoke("/", "POST", request, timeout, std::addressof(response), std::move(additional_params)); + if (r) + { + answer = response->m_body; + } + else + { + LOG_ERROR("POST request to Bitmessage failed: " << request.substr(0, 300)); + THROW_WALLET_EXCEPTION(tools::error::no_connection_to_bitmessage, m_bitmessage_url); + } + m_http_client.disconnect(); // see comment above + std::string string_value = get_str_between_tags(answer, "<string>", "</string>"); + if ((string_value.find("API Error") == 0) || (string_value.find("RPC ") == 0)) + { + THROW_WALLET_EXCEPTION(tools::error::bitmessage_api_error, string_value); + } + + return r; +} + +// Pick some string between two delimiters +// When parsing the XML returned by PyBitmessage, don't bother to fully parse it but as a little hack rely on the +// fact that e.g. a single string returned will be, however deeply nested in "<params><param><value>...", delivered +// between the very first "<string>" and "</string>" tags to be found in the XML +std::string message_transporter::get_str_between_tags(const std::string &s, const std::string &start_delim, const std::string &stop_delim) +{ + size_t first_delim_pos = s.find(start_delim); + if (first_delim_pos != std::string::npos) + { + size_t end_pos_of_first_delim = first_delim_pos + start_delim.length(); + size_t last_delim_pos = s.find(stop_delim); + if (last_delim_pos != std::string::npos) + { + return s.substr(end_pos_of_first_delim, last_delim_pos - end_pos_of_first_delim); + } + } + return std::string(); +} + +void message_transporter::start_xml_rpc_cmd(std::string &xml, const std::string &method_name) +{ + xml = (boost::format("<?xml version=\"1.0\"?><methodCall><methodName>%s</methodName><params>") % method_name).str(); +} + +void message_transporter::add_xml_rpc_string_param(std::string &xml, const std::string ¶m) +{ + xml += (boost::format("<param><value><string>%s</string></value></param>") % param).str(); +} + +void message_transporter::add_xml_rpc_base64_param(std::string &xml, const std::string ¶m) +{ + // Bitmessage expects some arguments Base64-encoded, but it wants them as parameters of type "string", not "base64" that is also part of XML-RPC + std::string encoded_param = epee::string_encoding::base64_encode(param); + xml += (boost::format("<param><value><string>%s</string></value></param>") % encoded_param).str(); +} + +void message_transporter::add_xml_rpc_integer_param(std::string &xml, const int32_t ¶m) +{ + xml += (boost::format("<param><value><int>%i</int></value></param>") % param).str(); +} + +void message_transporter::end_xml_rpc_cmd(std::string &xml) +{ + xml += "</params></methodCall>"; +} + +} diff --git a/src/wallet/message_transporter.h b/src/wallet/message_transporter.h new file mode 100644 index 000000000..8291311ce --- /dev/null +++ b/src/wallet/message_transporter.h @@ -0,0 +1,113 @@ +// Copyright (c) 2018, 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 "serialization/keyvalue_serialization.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "cryptonote_basic/cryptonote_boost_serialization.h" +#include "cryptonote_basic/account_boost_serialization.h" +#include "cryptonote_basic/cryptonote_basic.h" +#include "net/http_server_impl_base.h" +#include "net/http_client.h" +#include "common/util.h" +#include "wipeable_string.h" +#include "serialization/keyvalue_serialization.h" +#include <vector> + +namespace mms +{ + +struct transport_message +{ + cryptonote::account_public_address source_monero_address; + std::string source_transport_address; + cryptonote::account_public_address destination_monero_address; + std::string destination_transport_address; + crypto::chacha_iv iv; + crypto::public_key encryption_public_key; + uint64_t timestamp; + uint32_t type; + std::string subject; + std::string content; + crypto::hash hash; + crypto::signature signature; + uint32_t round; + uint32_t signature_count; + std::string transport_id; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(source_monero_address) + KV_SERIALIZE(source_transport_address) + KV_SERIALIZE(destination_monero_address) + KV_SERIALIZE(destination_transport_address) + KV_SERIALIZE_VAL_POD_AS_BLOB(iv) + KV_SERIALIZE_VAL_POD_AS_BLOB(encryption_public_key) + KV_SERIALIZE(timestamp) + KV_SERIALIZE(type) + KV_SERIALIZE(subject) + KV_SERIALIZE(content) + KV_SERIALIZE_VAL_POD_AS_BLOB(hash) + KV_SERIALIZE_VAL_POD_AS_BLOB(signature) + KV_SERIALIZE(round) + KV_SERIALIZE(signature_count) + KV_SERIALIZE(transport_id) + END_KV_SERIALIZE_MAP() +}; + +class message_transporter +{ +public: + message_transporter(); + void set_options(const std::string &bitmessage_address, const epee::wipeable_string &bitmessage_login); + bool send_message(const transport_message &message); + bool receive_messages(const std::vector<std::string> &destination_transport_addresses, + std::vector<transport_message> &messages); + bool delete_message(const std::string &transport_id); + void stop() { m_run.store(false, std::memory_order_relaxed); } + std::string derive_transport_address(const std::string &seed); + std::string derive_and_receive_transport_address(const std::string &seed); + bool delete_transport_address(const std::string &transport_address); + +private: + epee::net_utils::http::http_simple_client m_http_client; + std::string m_bitmessage_url; + epee::wipeable_string m_bitmessage_login; + std::atomic<bool> m_run; + + bool post_request(const std::string &request, std::string &answer); + static std::string get_str_between_tags(const std::string &s, const std::string &start_delim, const std::string &stop_delim); + + static void start_xml_rpc_cmd(std::string &xml, const std::string &method_name); + static void add_xml_rpc_string_param(std::string &xml, const std::string ¶m); + static void add_xml_rpc_base64_param(std::string &xml, const std::string ¶m); + static void add_xml_rpc_integer_param(std::string &xml, const int32_t ¶m); + static void end_xml_rpc_cmd(std::string &xml); + +}; + +} diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 097278961..0d2faca54 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -226,7 +226,7 @@ struct options { const command_line::arg_descriptor<std::string> tx_notify = { "tx-notify" , "Run a program for each new incoming transaction, '%s' will be replaced by the transaction hash" , "" }; }; -void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file) +void do_prepare_file_names(const std::string& file_path, std::string& keys_file, std::string& wallet_file, std::string &mms_file) { keys_file = file_path; wallet_file = file_path; @@ -238,6 +238,7 @@ void do_prepare_file_names(const std::string& file_path, std::string& keys_file, {//provided wallet file name keys_file += ".keys"; } + mms_file = file_path + ".mms"; } uint64_t calculate_fee(uint64_t fee_per_kb, size_t bytes, uint64_t fee_multiplier) @@ -330,6 +331,7 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl wallet->init(std::move(daemon_address), std::move(login), 0, false, *trusted_daemon); boost::filesystem::path ringdb_path = command_line::get_arg(vm, opts.shared_ringdb_dir); wallet->set_ring_database(ringdb_path.string()); + wallet->get_message_store().set_options(vm); wallet->device_name(device_name); wallet->device_derivation_path(device_derivation_path); @@ -907,6 +909,8 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended): m_light_wallet_connected(false), m_light_wallet_balance(0), m_light_wallet_unlocked_balance(0), + m_original_keys_available(false), + m_message_store(), m_key_device_type(hw::device::device_type::SOFTWARE), m_ring_history_saved(false), m_ringdb(), @@ -956,6 +960,7 @@ void wallet2::init_options(boost::program_options::options_description& desc_par command_line::add_arg(desc_params, opts.stagenet); command_line::add_arg(desc_params, opts.shared_ringdb_dir); command_line::add_arg(desc_params, opts.kdf_rounds); + mms::message_store::init_options(desc_params); command_line::add_arg(desc_params, opts.hw_device); command_line::add_arg(desc_params, opts.hw_device_derivation_path); command_line::add_arg(desc_params, opts.tx_notify); @@ -3184,6 +3189,9 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable value2.SetUint(m_subaddress_lookahead_minor); json.AddMember("subaddress_lookahead_minor", value2, json.GetAllocator()); + value2.SetInt(m_original_keys_available ? 1 : 0); + json.AddMember("original_keys_available", value2, json.GetAllocator()); + value2.SetUint(1); json.AddMember("encrypted_secret_keys", value2, json.GetAllocator()); @@ -3193,6 +3201,18 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable value.SetString(m_device_derivation_path.c_str(), m_device_derivation_path.size()); json.AddMember("device_derivation_path", value, json.GetAllocator()); + std::string original_address; + std::string original_view_secret_key; + if (m_original_keys_available) + { + original_address = get_account_address_as_str(m_nettype, false, m_original_address); + value.SetString(original_address.c_str(), original_address.length()); + json.AddMember("original_address", value, json.GetAllocator()); + original_view_secret_key = epee::string_tools::pod_to_hex(m_original_view_secret_key); + value.SetString(original_view_secret_key.c_str(), original_view_secret_key.length()); + json.AddMember("original_view_secret_key", value, json.GetAllocator()); + } + // Serialize the JSON object rapidjson::StringBuffer buffer; rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); @@ -3311,6 +3331,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ m_ignore_fractional_outputs = true; m_subaddress_lookahead_major = SUBADDRESS_LOOKAHEAD_MAJOR; m_subaddress_lookahead_minor = SUBADDRESS_LOOKAHEAD_MINOR; + m_original_keys_available = false; m_device_name = ""; m_device_derivation_path = ""; m_key_device_type = hw::device::device_type::SOFTWARE; @@ -3483,6 +3504,35 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, device_derivation_path, std::string, String, false, std::string()); m_device_derivation_path = field_device_derivation_path; + + if (json.HasMember("original_keys_available")) + { + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, original_keys_available, int, Int, false, false); + m_original_keys_available = field_original_keys_available; + if (m_original_keys_available) + { + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, original_address, std::string, String, true, std::string()); + address_parse_info info; + bool ok = get_account_address_from_str(info, m_nettype, field_original_address); + if (!ok) + { + LOG_ERROR("Failed to parse original_address from JSON"); + return false; + } + m_original_address = info.address; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, original_view_secret_key, std::string, String, true, std::string()); + ok = epee::string_tools::hex_to_pod(field_original_view_secret_key, m_original_view_secret_key); + if (!ok) + { + LOG_ERROR("Failed to parse original_view_secret_key from JSON"); + return false; + } + } + } + else + { + m_original_keys_available = false; + } } else { @@ -3809,6 +3859,10 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& m_key_device_type = hw::device::device_type::SOFTWARE; setup_keys(password); + // Not possible to restore a multisig wallet that is able to activate the MMS + // (because the original keys are not (yet) part of the restore info) + m_original_keys_available = false; + create_keys_file(wallet_, false, password, m_nettype != MAINNET || create_address_file); setup_new_blockchain(); @@ -3846,6 +3900,7 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); + m_original_keys_available = false; m_key_device_type = hw::device::device_type::SOFTWARE; setup_keys(password); @@ -3934,6 +3989,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); + m_original_keys_available = false; m_key_device_type = hw::device::device_type::SOFTWARE; setup_keys(password); @@ -3974,6 +4030,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); + m_original_keys_available = false; m_key_device_type = hw::device::device_type::SOFTWARE; setup_keys(password); @@ -4015,6 +4072,7 @@ void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& p m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); + m_original_keys_available = false; setup_keys(password); m_device_name = device_name; @@ -4127,6 +4185,15 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, m_multisig_derivations = derivations; } } + + if (!m_original_keys_available) + { + // Save the original i.e. non-multisig keys so the MMS can continue to use them to encrypt and decrypt messages + // (making a wallet multisig overwrites those keys, see account_base::make_multisig) + m_original_address = m_account.get_keys().m_account_address; + m_original_view_secret_key = m_account.get_keys().m_view_secret_key; + m_original_keys_available = true; + } clear(); MINFO("Creating view key..."); @@ -4560,8 +4627,8 @@ void wallet2::write_watch_only_wallet(const std::string& wallet_name, const epee //---------------------------------------------------------------------------------------------------- void wallet2::wallet_exists(const std::string& file_path, bool& keys_file_exists, bool& wallet_file_exists) { - std::string keys_file, wallet_file; - do_prepare_file_names(file_path, keys_file, wallet_file); + std::string keys_file, wallet_file, mms_file; + do_prepare_file_names(file_path, keys_file, wallet_file, mms_file); boost::system::error_code ignore; keys_file_exists = boost::filesystem::exists(keys_file, ignore); @@ -4615,7 +4682,7 @@ bool wallet2::parse_payment_id(const std::string& payment_id_str, crypto::hash& //---------------------------------------------------------------------------------------------------- bool wallet2::prepare_file_names(const std::string& file_path) { - do_prepare_file_names(file_path, m_keys_file, m_wallet_file); + do_prepare_file_names(file_path, m_keys_file, m_wallet_file, m_mms_file); return true; } //---------------------------------------------------------------------------------------------------- @@ -4808,6 +4875,8 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass { MERROR("Failed to save rings, will try again next time"); } + + m_message_store.read_from_file(get_multisig_wallet_state(), m_mms_file); } //---------------------------------------------------------------------------------------------------- void wallet2::trim_hashchain() @@ -4913,6 +4982,7 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas const std::string old_file = m_wallet_file; const std::string old_keys_file = m_keys_file; const std::string old_address_file = m_wallet_file + ".address.txt"; + const std::string old_mms_file = m_mms_file; // save keys to the new file // if we here, main wallet file is saved and we only need to save keys and address files @@ -4942,6 +5012,14 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas if (!r) { LOG_ERROR("error removing file: " << old_address_file); } + // remove old message store file + if (boost::filesystem::exists(old_mms_file)) + { + r = boost::filesystem::remove(old_mms_file); + if (!r) { + LOG_ERROR("error removing file: " << old_mms_file); + } + } } else { // save to new file #ifdef WIN32 @@ -4967,6 +5045,14 @@ void wallet2::store_to(const std::string &path, const epee::wipeable_string &pas std::error_code e = tools::replace_file(new_file, m_wallet_file); THROW_WALLET_EXCEPTION_IF(e, error::file_save_error, m_wallet_file, e); } + + if (m_message_store.get_active()) + { + // While the "m_message_store" object of course always exist, a file for the message + // store should only exist if the MMS is really active + m_message_store.write_to_file(get_multisig_wallet_state(), m_mms_file); + } + } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::balance(uint32_t index_major) const @@ -12030,6 +12116,29 @@ void wallet2::generate_genesis(cryptonote::block& b) const { cryptonote::generate_genesis_block(b, get_config(m_nettype).GENESIS_TX, get_config(m_nettype).GENESIS_NONCE); } //---------------------------------------------------------------------------------------------------- +mms::multisig_wallet_state wallet2::get_multisig_wallet_state() const +{ + mms::multisig_wallet_state state; + state.nettype = m_nettype; + state.multisig = multisig(&state.multisig_is_ready); + state.has_multisig_partial_key_images = has_multisig_partial_key_images(); + state.multisig_rounds_passed = m_multisig_rounds_passed; + state.num_transfer_details = m_transfers.size(); + if (state.multisig) + { + THROW_WALLET_EXCEPTION_IF(!m_original_keys_available, error::wallet_internal_error, "MMS use not possible because own original Monero address not available"); + state.address = m_original_address; + state.view_secret_key = m_original_view_secret_key; + } + else + { + state.address = m_account.get_keys().m_account_address; + state.view_secret_key = m_account.get_keys().m_view_secret_key; + } + state.mms_file=m_mms_file; + return state; +} +//---------------------------------------------------------------------------------------------------- wallet_device_callback * wallet2::get_device_callback() { if (!m_device_callback){ diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 91c68bf3c..ace01a235 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -61,6 +61,7 @@ #include "wallet_errors.h" #include "common/password.h" #include "node_rpc_proxy.h" +#include "message_store.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2" @@ -674,7 +675,7 @@ namespace tools bool init(std::string daemon_address = "http://localhost:8080", boost::optional<epee::net_utils::http::login> daemon_login = boost::none, uint64_t upper_transaction_weight_limit = 0, bool ssl = false, bool trusted_daemon = false); - void stop() { m_run.store(false, std::memory_order_relaxed); } + void stop() { m_run.store(false, std::memory_order_relaxed); m_message_store.stop(); } i_wallet2_callback* callback() const { return m_callback; } void callback(i_wallet2_callback* callback) { m_callback = callback; } @@ -1218,6 +1219,11 @@ namespace tools bool unblackball_output(const std::pair<uint64_t, uint64_t> &output); bool is_output_blackballed(const std::pair<uint64_t, uint64_t> &output) const; + // MMS ------------------------------------------------------------------------------------------------- + mms::message_store& get_message_store() { return m_message_store; }; + const mms::message_store& get_message_store() const { return m_message_store; }; + mms::multisig_wallet_state get_multisig_wallet_state() const; + bool lock_keys_file(); bool unlock_keys_file(); bool is_keys_file_locked() const; @@ -1320,6 +1326,7 @@ namespace tools std::string m_daemon_address; std::string m_wallet_file; std::string m_keys_file; + std::string m_mms_file; epee::net_utils::http::http_simple_client m_http_client; hashchain m_blockchain; std::unordered_map<crypto::hash, unconfirmed_transfer_details> m_unconfirmed_txs; @@ -1420,6 +1427,11 @@ namespace tools uint64_t m_last_block_reward; std::unique_ptr<tools::file_locker> m_keys_file_locker; + + mms::message_store m_message_store; + bool m_original_keys_available; + cryptonote::account_public_address m_original_address; + crypto::secret_key m_original_view_secret_key; crypto::chacha_key m_cache_key; boost::optional<epee::wipeable_string> m_encrypt_keys_after_refresh; diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index e2caee5d2..35862bda1 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -821,6 +821,31 @@ namespace tools std::string m_wallet_file; }; //---------------------------------------------------------------------------------------------------- + struct mms_error : public wallet_logic_error + { + protected: + explicit mms_error(std::string&& loc, const std::string& message) + : wallet_logic_error(std::move(loc), message) + { + } + }; + //---------------------------------------------------------------------------------------------------- + struct no_connection_to_bitmessage : public mms_error + { + explicit no_connection_to_bitmessage(std::string&& loc, const std::string& address) + : mms_error(std::move(loc), "no connection to PyBitmessage at address " + address) + { + } + }; + //---------------------------------------------------------------------------------------------------- + struct bitmessage_api_error : public mms_error + { + explicit bitmessage_api_error(std::string&& loc, const std::string& error_string) + : mms_error(std::move(loc), "PyBitmessage returned " + error_string) + { + } + }; + //---------------------------------------------------------------------------------------------------- #if !defined(_MSC_VER) |