// Copyright (c) 2017-2019, 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 "device_trezor.hpp" namespace hw { namespace trezor { #ifdef WITH_DEVICE_TREZOR #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "device.trezor" #define HW_TREZOR_NAME "Trezor" static device_trezor *trezor_device = nullptr; static device_trezor *ensure_trezor_device(){ if (!trezor_device) { trezor_device = new device_trezor(); trezor_device->set_name(HW_TREZOR_NAME); } return trezor_device; } void register_all(std::map> ®istry) { registry.insert(std::make_pair(HW_TREZOR_NAME, std::unique_ptr(ensure_trezor_device()))); } void register_all() { hw::register_device(HW_TREZOR_NAME, ensure_trezor_device()); } device_trezor::device_trezor() { m_live_refresh_in_progress = false; m_live_refresh_enabled = true; m_live_refresh_thread_running = false; } device_trezor::~device_trezor() { try { disconnect(); release(); } catch(std::exception const& e){ MWARNING("Could not disconnect and release: " << e.what()); } } bool device_trezor::init() { m_live_refresh_in_progress = false; bool r = device_trezor_base::init(); if (r && !m_live_refresh_thread) { m_live_refresh_thread_running = true; m_live_refresh_thread.reset(new boost::thread(boost::bind(&device_trezor::live_refresh_thread_main, this))); } return r; } bool device_trezor::release() { m_live_refresh_in_progress = false; m_live_refresh_thread_running = false; if (m_live_refresh_thread) { m_live_refresh_thread->join(); m_live_refresh_thread = nullptr; } return device_trezor_base::release(); } bool device_trezor::disconnect() { m_live_refresh_in_progress = false; return device_trezor_base::disconnect(); } void device_trezor::device_state_reset_unsafe() { require_connected(); if (m_live_refresh_in_progress) { try { live_refresh_finish_unsafe(); } catch(const std::exception & e) { MERROR("Live refresh could not be terminated: " << e.what()); } } m_live_refresh_in_progress = false; device_trezor_base::device_state_reset_unsafe(); } void device_trezor::live_refresh_thread_main() { while(m_live_refresh_thread_running) { boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); if (!m_live_refresh_in_progress) { continue; } TREZOR_AUTO_LOCK_DEVICE(); if (!m_transport || !m_live_refresh_in_progress) { continue; } auto current_time = std::chrono::steady_clock::now(); if (current_time - m_last_live_refresh_time <= std::chrono::minutes(5)) { continue; } MTRACE("Closing live refresh process due to inactivity"); try { live_refresh_finish(); } catch(const std::exception &e) { MWARNING("Live refresh auto-finish failed: " << e.what()); } } } /* ======================================================================= */ /* WALLET & ADDRESS */ /* ======================================================================= */ bool device_trezor::get_public_address(cryptonote::account_public_address &pubkey) { try { auto res = get_address(); cryptonote::address_parse_info info{}; bool r = cryptonote::get_account_address_from_str(info, this->network_type, res->address()); CHECK_AND_ASSERT_MES(r, false, "Could not parse returned address. Address parse failed: " + res->address()); CHECK_AND_ASSERT_MES(!info.is_subaddress, false, "Trezor returned a sub address"); pubkey = info.address; return true; } catch(std::exception const& e){ MERROR("Get public address exception: " << e.what()); return false; } } bool device_trezor::get_secret_keys(crypto::secret_key &viewkey , crypto::secret_key &spendkey) { try { MDEBUG("Loading view-only key from the Trezor. Please check the Trezor for a confirmation."); auto res = get_view_key(); CHECK_AND_ASSERT_MES(res->watch_key().size() == 32, false, "Trezor returned invalid view key"); // Trezor does not make use of spendkey of the device API. // Ledger loads encrypted spendkey, Trezor loads null key (never leaves device). // In the test (debugging mode) we need to leave this field intact as it is already set by // the debugging code and need to remain same for the testing purposes. #ifndef WITH_TREZOR_DEBUGGING spendkey = crypto::null_skey; // not given #endif memcpy(viewkey.data, res->watch_key().data(), 32); return true; } catch(std::exception const& e){ MERROR("Get secret keys exception: " << e.what()); return false; } } /* ======================================================================= */ /* Helpers */ /* ======================================================================= */ /* ======================================================================= */ /* TREZOR PROTOCOL */ /* ======================================================================= */ std::shared_ptr device_trezor::get_address( const boost::optional> & path, const boost::optional & network_type){ TREZOR_AUTO_LOCK_CMD(); require_connected(); device_state_reset_unsafe(); require_initialized(); auto req = std::make_shared(); this->set_msg_addr(req.get(), path, network_type); auto response = this->client_exchange(req); MTRACE("Get address response received"); return response; } std::shared_ptr device_trezor::get_view_key( const boost::optional> & path, const boost::optional & network_type){ TREZOR_AUTO_LOCK_CMD(); require_connected(); device_state_reset_unsafe(); require_initialized(); auto req = std::make_shared(); this->set_msg_addr(req.get(), path, network_type); auto response = this->client_exchange(req); MTRACE("Get watch key response received"); return response; } bool device_trezor::is_get_tx_key_supported() const { require_initialized(); return get_version() > pack_version(2, 0, 10); } void device_trezor::load_tx_key_data(::hw::device_cold::tx_key_data_t & res, const std::string & tx_aux_data) { protocol::tx::load_tx_key_data(res, tx_aux_data); } void device_trezor::get_tx_key( std::vector<::crypto::secret_key> & tx_keys, const ::hw::device_cold::tx_key_data_t & tx_aux_data, const ::crypto::secret_key & view_key_priv) { TREZOR_AUTO_LOCK_CMD(); require_connected(); device_state_reset_unsafe(); require_initialized(); auto req = protocol::tx::get_tx_key(tx_aux_data); this->set_msg_addr(req.get()); auto response = this->client_exchange(req); MTRACE("Get TX key response received"); protocol::tx::get_tx_key_ack(tx_keys, tx_aux_data.tx_prefix_hash, view_key_priv, response); } void device_trezor::ki_sync(wallet_shim * wallet, const std::vector & transfers, hw::device_cold::exported_key_image & ski) { #define EVENT_PROGRESS(P) do { if (m_callback) {(m_callback)->on_progress(device_cold::op_progress(P)); } }while(0) TREZOR_AUTO_LOCK_CMD(); require_connected(); device_state_reset_unsafe(); require_initialized(); std::shared_ptr req; std::vector mtds; std::vector kis; protocol::ki::key_image_data(wallet, transfers, mtds); protocol::ki::generate_commitment(mtds, transfers, req); EVENT_PROGRESS(0.); this->set_msg_addr(req.get()); auto ack1 = this->client_exchange(req); const auto batch_size = 10; const auto num_batches = (mtds.size() + batch_size - 1) / batch_size; for(uint64_t cur = 0; cur < num_batches; ++cur){ auto step_req = std::make_shared(); auto idx_finish = std::min(static_cast((cur + 1) * batch_size), static_cast(mtds.size())); for(uint64_t idx = cur * batch_size; idx < idx_finish; ++idx){ auto added_tdis = step_req->add_tdis(); CHECK_AND_ASSERT_THROW_MES(idx < mtds.size(), "Invalid transfer detail index"); *added_tdis = mtds[idx]; } auto step_ack = this->client_exchange(step_req); auto kis_size = step_ack->kis_size(); kis.reserve(static_cast(kis_size)); for(int i = 0; i < kis_size; ++i){ auto ckis = step_ack->kis(i); kis.push_back(ckis); } MTRACE("Batch " << cur << " / " << num_batches << " batches processed"); EVENT_PROGRESS((double)cur * batch_size / mtds.size()); } EVENT_PROGRESS(1.); auto final_req = std::make_shared(); auto final_ack = this->client_exchange(final_req); ski.reserve(kis.size()); for(auto & sub : kis){ ::crypto::signature sig{}; ::crypto::key_image ki; char buff[sizeof(ki.data)*3]; size_t buff_len = sizeof(buff); protocol::crypto::chacha::decrypt(sub.blob().data(), sub.blob().size(), reinterpret_cast(final_ack->enc_key().data()), reinterpret_cast(sub.iv().data()), buff, &buff_len); CHECK_AND_ASSERT_THROW_MES(buff_len == sizeof(buff), "Plaintext size invalid"); memcpy(ki.data, buff, sizeof(ki.data)); memcpy(sig.c.data, buff + sizeof(ki.data), sizeof(ki.data)); memcpy(sig.r.data, buff + 2*sizeof(ki.data), sizeof(ki.data)); ski.push_back(std::make_pair(ki, sig)); } #undef EVENT_PROGRESS } bool device_trezor::is_live_refresh_supported() const { require_initialized(); return get_version() > pack_version(2, 0, 10); } bool device_trezor::is_live_refresh_enabled() const { return is_live_refresh_supported() && (mode == NONE || mode == TRANSACTION_PARSE) && m_live_refresh_enabled; } bool device_trezor::has_ki_live_refresh() const { try{ return is_live_refresh_enabled(); } catch(const std::exception & e){ MERROR("Could not detect if live refresh is enabled: " << e.what()); } return false; } void device_trezor::live_refresh_start() { TREZOR_AUTO_LOCK_CMD(); require_connected(); live_refresh_start_unsafe(); } void device_trezor::live_refresh_start_unsafe() { device_state_reset_unsafe(); require_initialized(); auto req = std::make_shared(); this->set_msg_addr(req.get()); this->client_exchange(req); m_live_refresh_in_progress = true; m_last_live_refresh_time = std::chrono::steady_clock::now(); } void device_trezor::live_refresh( const ::crypto::secret_key & view_key_priv, const crypto::public_key& out_key, const crypto::key_derivation& recv_derivation, size_t real_output_index, const cryptonote::subaddress_index& received_index, cryptonote::keypair& in_ephemeral, crypto::key_image& ki ) { TREZOR_AUTO_LOCK_CMD(); require_connected(); if (!m_live_refresh_in_progress) { live_refresh_start_unsafe(); } m_last_live_refresh_time = std::chrono::steady_clock::now(); auto req = std::make_shared(); req->set_out_key(out_key.data, 32); req->set_recv_deriv(recv_derivation.data, 32); req->set_real_out_idx(real_output_index); req->set_sub_addr_major(received_index.major); req->set_sub_addr_minor(received_index.minor); auto ack = this->client_exchange(req); protocol::ki::live_refresh_ack(view_key_priv, out_key, ack, in_ephemeral, ki); } void device_trezor::live_refresh_finish_unsafe() { auto req = std::make_shared(); this->client_exchange(req); m_live_refresh_in_progress = false; } void device_trezor::live_refresh_finish() { TREZOR_AUTO_LOCK_CMD(); require_connected(); if (m_live_refresh_in_progress) { live_refresh_finish_unsafe(); } } void device_trezor::computing_key_images(bool started) { try { if (!is_live_refresh_enabled()) { return; } // React only on termination as the process can auto-start itself. if (!started && m_live_refresh_in_progress) { live_refresh_finish(); } } catch(const std::exception & e) { MWARNING("KI computation state change failed, started: " << started << ", e: " << e.what()); } } bool device_trezor::compute_key_image( const ::cryptonote::account_keys& ack, const ::crypto::public_key& out_key, const ::crypto::key_derivation& recv_derivation, size_t real_output_index, const ::cryptonote::subaddress_index& received_index, ::cryptonote::keypair& in_ephemeral, ::crypto::key_image& ki) { if (!is_live_refresh_enabled()) { return false; } live_refresh(ack.m_view_secret_key, out_key, recv_derivation, real_output_index, received_index, in_ephemeral, ki); return true; } void device_trezor::tx_sign(wallet_shim * wallet, const tools::wallet2::unsigned_tx_set & unsigned_tx, tools::wallet2::signed_tx_set & signed_tx, hw::tx_aux_data & aux_data) { CHECK_AND_ASSERT_THROW_MES(unsigned_tx.transfers.first == 0, "Unsuported non zero offset"); TREZOR_AUTO_LOCK_CMD(); require_connected(); device_state_reset_unsafe(); require_initialized(); transaction_versions_check(unsigned_tx, aux_data); const size_t num_tx = unsigned_tx.txes.size(); m_num_transations_to_sign = num_tx; signed_tx.key_images.clear(); signed_tx.key_images.resize(unsigned_tx.transfers.second.size()); for(size_t tx_idx = 0; tx_idx < num_tx; ++tx_idx) { std::shared_ptr signer; tx_sign(wallet, unsigned_tx, tx_idx, aux_data, signer); auto & cdata = signer->tdata(); auto aux_info_cur = signer->store_tx_aux_info(); aux_data.tx_device_aux.emplace_back(aux_info_cur); // Pending tx reconstruction signed_tx.ptx.emplace_back(); auto & cpend = signed_tx.ptx.back(); cpend.tx = cdata.tx; cpend.dust = 0; cpend.fee = 0; cpend.dust_added_to_fee = false; cpend.change_dts = cdata.tx_data.change_dts; cpend.selected_transfers = cdata.tx_data.selected_transfers; cpend.key_images = ""; cpend.dests = cdata.tx_data.dests; cpend.construction_data = cdata.tx_data; // Transaction check try { transaction_check(cdata, aux_data); } catch(const std::exception &e){ throw exc::ProtocolException(std::string("Transaction verification failed: ") + e.what()); } std::string key_images; bool all_are_txin_to_key = std::all_of(cdata.tx.vin.begin(), cdata.tx.vin.end(), [&](const cryptonote::txin_v& s_e) -> bool { CHECKED_GET_SPECIFIC_VARIANT(s_e, const cryptonote::txin_to_key, in, false); key_images += boost::to_string(in.k_image) + " "; return true; }); if(!all_are_txin_to_key) { throw std::invalid_argument("Not all are txin_to_key"); } cpend.key_images = key_images; // KI sync for(size_t cidx=0, trans_max=unsigned_tx.transfers.second.size(); cidx < trans_max; ++cidx){ signed_tx.key_images[cidx] = unsigned_tx.transfers.second[cidx].m_key_image; } size_t num_sources = cdata.tx_data.sources.size(); CHECK_AND_ASSERT_THROW_MES(num_sources == cdata.source_permutation.size(), "Invalid permutation size"); CHECK_AND_ASSERT_THROW_MES(num_sources == cdata.tx.vin.size(), "Invalid tx.vin size"); for(size_t src_idx = 0; src_idx < num_sources; ++src_idx){ size_t idx_mapped = cdata.source_permutation[src_idx]; CHECK_AND_ASSERT_THROW_MES(idx_mapped < cdata.tx_data.selected_transfers.size(), "Invalid idx_mapped"); CHECK_AND_ASSERT_THROW_MES(src_idx < cdata.tx.vin.size(), "Invalid idx_mapped"); size_t idx_map_src = cdata.tx_data.selected_transfers[idx_mapped]; CHECK_AND_ASSERT_THROW_MES(idx_map_src >= unsigned_tx.transfers.first, "Invalid offset"); idx_map_src -= unsigned_tx.transfers.first; CHECK_AND_ASSERT_THROW_MES(idx_map_src < signed_tx.key_images.size(), "Invalid key image index"); const auto vini = boost::get(cdata.tx.vin[src_idx]); signed_tx.key_images[idx_map_src] = vini.k_image; } } if (m_callback){ m_callback->on_progress(device_cold::tx_progress(m_num_transations_to_sign, m_num_transations_to_sign, 1, 1, 1, 1)); } } void device_trezor::tx_sign(wallet_shim * wallet, const tools::wallet2::unsigned_tx_set & unsigned_tx, size_t idx, hw::tx_aux_data & aux_data, std::shared_ptr & signer) { #define EVENT_PROGRESS(S, SUB, SUBMAX) do { if (m_callback) { \ (m_callback)->on_progress(device_cold::tx_progress(idx, m_num_transations_to_sign, S, 10, SUB, SUBMAX)); \ } }while(0) require_connected(); if (idx > 0) device_state_reset_unsafe(); require_initialized(); EVENT_PROGRESS(0, 1, 1); CHECK_AND_ASSERT_THROW_MES(idx < unsigned_tx.txes.size(), "Invalid transaction index"); signer = std::make_shared(wallet, &unsigned_tx, idx, &aux_data); const tools::wallet2::tx_construction_data & cur_tx = unsigned_tx.txes[idx]; unsigned long num_sources = cur_tx.sources.size(); unsigned long num_outputs = cur_tx.splitted_dsts.size(); // Step: Init auto init_msg = signer->step_init(); this->set_msg_addr(init_msg.get()); transaction_pre_check(init_msg); EVENT_PROGRESS(1, 1, 1); auto response = this->client_exchange(init_msg); signer->step_init_ack(response); // Step: Set transaction inputs for(size_t cur_src = 0; cur_src < num_sources; ++cur_src){ auto src = signer->step_set_input(cur_src); auto ack = this->client_exchange(src); signer->step_set_input_ack(ack); EVENT_PROGRESS(2, cur_src, num_sources); } // Step: sort auto perm_req = signer->step_permutation(); if (perm_req){ auto perm_ack = this->client_exchange(perm_req); signer->step_permutation_ack(perm_ack); } EVENT_PROGRESS(3, 1, 1); // Step: input_vini for(size_t cur_src = 0; cur_src < num_sources; ++cur_src){ auto src = signer->step_set_vini_input(cur_src); auto ack = this->client_exchange(src); signer->step_set_vini_input_ack(ack); EVENT_PROGRESS(4, cur_src, num_sources); } // Step: all inputs set auto all_inputs_set = signer->step_all_inputs_set(); auto ack_all_inputs = this->client_exchange(all_inputs_set); signer->step_all_inputs_set_ack(ack_all_inputs); EVENT_PROGRESS(5, 1, 1); // Step: outputs for(size_t cur_dst = 0; cur_dst < num_outputs; ++cur_dst){ auto src = signer->step_set_output(cur_dst); auto ack = this->client_exchange(src); signer->step_set_output_ack(ack); // If BP is offloaded to host, another step with computed BP may be needed. auto offloaded_bp = signer->step_rsig(cur_dst); if (offloaded_bp){ auto bp_ack = this->client_exchange(offloaded_bp); signer->step_set_rsig_ack(ack); } EVENT_PROGRESS(6, cur_dst, num_outputs); } // Step: all outs set auto all_out_set = signer->step_all_outs_set(); auto ack_all_out_set = this->client_exchange(all_out_set); signer->step_all_outs_set_ack(ack_all_out_set, *this); EVENT_PROGRESS(7, 1, 1); // Step: sign each input for(size_t cur_src = 0; cur_src < num_sources; ++cur_src){ auto src = signer->step_sign_input(cur_src); auto ack_sign = this->client_exchange(src); signer->step_sign_input_ack(ack_sign); EVENT_PROGRESS(8, cur_src, num_sources); } // Step: final auto final_msg = signer->step_final(); auto ack_final = this->client_exchange(final_msg); signer->step_final_ack(ack_final); EVENT_PROGRESS(9, 1, 1); #undef EVENT_PROGRESS } void device_trezor::transaction_versions_check(const ::tools::wallet2::unsigned_tx_set & unsigned_tx, hw::tx_aux_data & aux_data) { auto trezor_version = get_version(); unsigned client_version = 1; // default client version for tx if (trezor_version <= pack_version(2, 0, 10)){ client_version = 0; } if (aux_data.client_version){ auto wanted_client_version = aux_data.client_version.get(); if (wanted_client_version > client_version){ throw exc::TrezorException("Trezor firmware 2.0.10 and lower does not support current transaction sign protocol. Please update."); } else { client_version = wanted_client_version; } } aux_data.client_version = client_version; if (client_version == 0 && aux_data.bp_version && aux_data.bp_version.get() != 1){ throw exc::TrezorException("Trezor firmware 2.0.10 and lower does not support current transaction sign protocol (BPv2+). Please update."); } } void device_trezor::transaction_pre_check(std::shared_ptr init_msg) { CHECK_AND_ASSERT_THROW_MES(init_msg, "TransactionInitRequest is empty"); CHECK_AND_ASSERT_THROW_MES(init_msg->has_tsx_data(), "TransactionInitRequest has no transaction data"); CHECK_AND_ASSERT_THROW_MES(m_features, "Device state not initialized"); // make sure the caller did not reset features const bool nonce_required = init_msg->tsx_data().has_payment_id() && init_msg->tsx_data().payment_id().size() > 0; if (nonce_required && init_msg->tsx_data().payment_id().size() == 8){ // Versions 2.0.9 and lower do not support payment ID if (get_version() <= pack_version(2, 0, 9)) { throw exc::TrezorException("Trezor firmware 2.0.9 and lower does not support transactions with short payment IDs or integrated addresses. Please update."); } } } void device_trezor::transaction_check(const protocol::tx::TData & tdata, const hw::tx_aux_data & aux_data) { // Simple serialization check cryptonote::blobdata tx_blob; cryptonote::transaction tx_deserialized; bool r = cryptonote::t_serializable_object_to_blob(tdata.tx, tx_blob); CHECK_AND_ASSERT_THROW_MES(r, "Transaction serialization failed"); r = cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx_deserialized); CHECK_AND_ASSERT_THROW_MES(r, "Transaction deserialization failed"); // Extras check std::vector tx_extra_fields; cryptonote::tx_extra_nonce nonce; r = cryptonote::parse_tx_extra(tdata.tx.extra, tx_extra_fields); CHECK_AND_ASSERT_THROW_MES(r, "tx.extra parsing failed"); const bool nonce_required = tdata.tsx_data.has_payment_id() && tdata.tsx_data.payment_id().size() > 0; const bool has_nonce = cryptonote::find_tx_extra_field_by_type(tx_extra_fields, nonce); CHECK_AND_ASSERT_THROW_MES(has_nonce || !nonce_required, "Transaction nonce not present"); if (nonce_required){ const std::string & payment_id = tdata.tsx_data.payment_id(); if (payment_id.size() == 32){ crypto::hash payment_id_long{}; CHECK_AND_ASSERT_THROW_MES(cryptonote::get_payment_id_from_tx_extra_nonce(nonce.nonce, payment_id_long), "Long payment ID not present"); } else if (payment_id.size() == 8){ crypto::hash8 payment_id_short{}; CHECK_AND_ASSERT_THROW_MES(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(nonce.nonce, payment_id_short), "Short payment ID not present"); } } } #else //WITH_DEVICE_TREZOR void register_all(std::map> ®istry) { } void register_all() { } #endif //WITH_DEVICE_TREZOR }}