diff --git a/src/apps/monero/README.md b/src/apps/monero/README.md index ac9f5628e..e950b35d6 100644 --- a/src/apps/monero/README.md +++ b/src/apps/monero/README.md @@ -172,6 +172,8 @@ to the agent/hot-wallet so it can decrypt computed KIs and import it For detailed description and rationale please refer to the [monero-doc]. +TODO + - The wrapping message: `MoneroTransactionSignRequest`. - The main multiplexor: `apps/monero/protocol/tsx_sign.py` - The main signing logic is implemented in `apps/monero/protocol/tsx_sign_builder.py` diff --git a/src/apps/monero/protocol/signing/state.py b/src/apps/monero/protocol/signing/state.py new file mode 100644 index 000000000..637568a90 --- /dev/null +++ b/src/apps/monero/protocol/signing/state.py @@ -0,0 +1,125 @@ +import gc +from micropython import const + +from trezor import log + +from apps.monero.xmr import crypto + + +class TprefixStub: + __slots__ = ("version", "unlock_time", "vin", "vout", "extra") + + def __init__(self, **kwargs): + for kw in kwargs: + setattr(self, kw, kwargs[kw]) + + +class State: + + STEP_INP = const(100) + STEP_PERM = const(200) + STEP_VINI = const(300) + STEP_ALL_IN = const(350) + STEP_OUT = const(400) + STEP_ALL_OUT = const(500) + STEP_MLSAG = const(600) + STEP_SIGN = const(700) + + def __init__(self, ctx): + self.ctx = ctx + self.creds = None + self.key_master = None + self.key_hmac = None + self.key_enc = None + + self.tx_priv = None # txkey + self.tx_pub = None + + self.multi_sig = False + self.need_additional_txkeys = False + self.use_bulletproof = False + self.use_rct = True + self.use_simple_rct = False + self.input_count = 0 + self.output_count = 0 + self.output_change = None + self.mixin = 0 + self.fee = 0 + self.account_idx = 0 + + self.additional_tx_private_keys = [] + self.additional_tx_public_keys = [] + self.inp_idx = -1 + self.out_idx = -1 + self.summary_inputs_money = 0 + self.summary_outs_money = 0 + self.input_secrets = [] + self.input_alphas = [] + self.input_pseudo_outs = [] + self.output_sk = [] + self.output_pk = [] + self.output_amounts = [] + self.output_masks = [] + self.rsig_type = 0 + self.rsig_grp = [] + self.rsig_offload = 0 + self.sumout = crypto.sc_0() + self.sumpouts_alphas = crypto.sc_0() + self.subaddresses = {} + self.tx = None + self.source_permutation = [] # sorted by key images + self.tx_prefix_hasher = None + self.tx_prefix_hash = None + self.full_message_hasher = None + self.full_message = None + self.exp_tx_prefix_hash = None + self._init() + + def _init(self): + from apps.monero.xmr.sub.keccak_hasher import KeccakXmrArchive + from apps.monero.xmr.sub.mlsag_hasher import PreMlsagHasher + + self.tx = TprefixStub(vin=[], vout=[], extra=b"") + self.tx_prefix_hasher = KeccakXmrArchive() + self.full_message_hasher = PreMlsagHasher() + + def _mem_trace(self, x=None, collect=False): + if __debug__: + log.debug( + __name__, + "Log trace: %s, ... F: %s A: %s", + x, + gc.mem_free(), + gc.mem_alloc(), + ) + if collect: + gc.collect() + + def assrt(self, condition, msg=None): + if condition: + return + raise ValueError("Assertion error%s" % (" : %s" % msg if msg else "")) + + def num_inputs(self): + return self.input_count + + def num_dests(self): + return self.output_count + + def get_fee(self): + return self.fee if self.fee > 0 else 0 + + def change_address(self): + return self.output_change.addr if self.output_change else None + + def get_rct_type(self): + """ + RCTsig type (simple/full x Borromean/Bulletproof) + :return: + """ + from apps.monero.xmr.serialize_messages.tx_rsig import RctType + + if self.use_simple_rct: + return RctType.FullBulletproof if self.use_bulletproof else RctType.Simple + else: + return RctType.Full diff --git a/src/apps/monero/protocol/signing/step_01_init_transaction.py b/src/apps/monero/protocol/signing/step_01_init_transaction.py new file mode 100644 index 000000000..e7bcac44d --- /dev/null +++ b/src/apps/monero/protocol/signing/step_01_init_transaction.py @@ -0,0 +1,252 @@ +""" +Initializes a new transaction. +""" + +import gc + +from apps.monero.controller import misc +from apps.monero.layout import confirms +from apps.monero.protocol.signing.state import State +from apps.monero.xmr import common, crypto, monero + + +async def init_transaction(ctx, address_n, network_type, tsx_data): + from apps.monero.xmr.sub.addr import classify_subaddresses + from apps.monero.protocol import hmac_encryption_keys + + state = State(ctx) + state.creds = await misc.monero_get_creds(ctx, address_n, network_type) + + state.tx_priv = crypto.random_scalar() + state.tx_pub = crypto.scalarmult_base(state.tx_priv) + + state._mem_trace(1) + + # Ask for confirmation + await confirms.confirm_transaction(state.ctx, tsx_data, state.creds) + gc.collect() + state._mem_trace(3) + + # Basic transaction parameters + state.input_count = tsx_data.num_inputs + state.output_count = len(tsx_data.outputs) + state.output_change = misc.dst_entry_to_stdobj(tsx_data.change_dts) + state.mixin = tsx_data.mixin + state.fee = tsx_data.fee + state.account_idx = tsx_data.account + state.multi_sig = tsx_data.is_multisig + check_change(state, tsx_data.outputs) + state.exp_tx_prefix_hash = tsx_data.exp_tx_prefix_hash + + # Rsig data + state.rsig_type = tsx_data.rsig_data.rsig_type + state.rsig_grp = tsx_data.rsig_data.grouping + state.rsig_offload = state.rsig_type > 0 and state.output_count > 2 + state.use_bulletproof = state.rsig_type > 0 + state.use_simple_rct = state.input_count > 1 or state.rsig_type != 0 + + # Provided tx key, used mostly in multisig. + if len(tsx_data.use_tx_keys) > 0: + for ckey in tsx_data.use_tx_keys: + crypto.check_sc(crypto.decodeint(ckey)) + + state.tx_priv = crypto.decodeint(tsx_data.use_tx_keys[0]) + state.tx_pub = crypto.scalarmult_base(state.tx_priv) + state.additional_tx_private_keys = [ + crypto.decodeint(x) for x in tsx_data.use_tx_keys[1:] + ] + + # Additional keys w.r.t. subaddress destinations + class_res = classify_subaddresses(tsx_data.outputs, state.change_address()) + num_stdaddresses, num_subaddresses, single_dest_subaddress = class_res + + # if this is a single-destination transfer to a subaddress, we set the tx pubkey to R=r*D + if num_stdaddresses == 0 and num_subaddresses == 1: + state.tx_pub = crypto.scalarmult( + crypto.decodepoint(single_dest_subaddress.spend_public_key), state.tx_priv + ) + + state.need_additional_txkeys = num_subaddresses > 0 and ( + num_stdaddresses > 0 or num_subaddresses > 1 + ) + state._mem_trace(4, True) + + # Extra processing, payment id + state.tx.version = 2 + state.tx.unlock_time = tsx_data.unlock_time + process_payment_id(state, tsx_data) + await compute_sec_keys(state, tsx_data) + gc.collect() + + # Iterative tx_prefix_hash hash computation + state.tx_prefix_hasher.keep() + state.tx_prefix_hasher.uvarint(state.tx.version) + state.tx_prefix_hasher.uvarint(state.tx.unlock_time) + state.tx_prefix_hasher.container_size(state.input_count) # ContainerType + state.tx_prefix_hasher.release() + state._mem_trace(10, True) + + # Final message hasher + state.full_message_hasher.init(state.use_simple_rct) + state.full_message_hasher.set_type_fee(state.get_rct_type(), state.get_fee()) + + # Sub address precomputation + if tsx_data.account is not None and tsx_data.minor_indices: + precompute_subaddr(state, tsx_data.account, tsx_data.minor_indices) + state._mem_trace(5, True) + + # HMAC outputs - pinning + hmacs = [] + for idx in range(state.num_dests()): + c_hmac = await hmac_encryption_keys.gen_hmac_tsxdest( + state.key_hmac, tsx_data.outputs[idx], idx + ) + hmacs.append(c_hmac) + gc.collect() + + state._mem_trace(6) + + from trezor.messages.MoneroTransactionInitAck import MoneroTransactionInitAck + from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData + + rsig_data = MoneroTransactionRsigData(offload_type=state.rsig_offload) + + result_msg = MoneroTransactionInitAck( + in_memory=False, + many_inputs=True, + many_outputs=True, + hmacs=hmacs, + rsig_data=rsig_data, + ) + return await dispatch_and_forward(state, result_msg) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_02_set_input + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read((MessageType.MoneroTransactionSetInputRequest,)) + + return await step_02_set_input.set_input(state, received_msg.src_entr) + + +def get_primary_change_address(state: State): + """ + Computes primary change address for the current account index + """ + D, C = monero.generate_sub_address_keys( + state.creds.view_key_private, state.creds.spend_key_public, state.account_idx, 0 + ) + return misc.StdObj( + view_public_key=crypto.encodepoint(C), spend_public_key=crypto.encodepoint(D) + ) + + +def check_change(state: State, outputs): + """ + Checks if the change address is among tx outputs and it is equal to our address. + """ + from apps.monero.xmr.sub.addr import addr_eq, get_change_addr_idx + + change_idx = get_change_addr_idx(outputs, state.output_change) + + change_addr = state.change_address() + if change_addr is None: + state._mem_trace("No change" if __debug__ else None) + return + + if change_idx is None and state.output_change.amount == 0 and len(outputs) == 2: + state._mem_trace("Sweep tsx" if __debug__ else None) + return # sweep dummy tsx + + found = False + for out in outputs: + if addr_eq(out.addr, change_addr): + found = True + break + + if not found: + raise misc.TrezorChangeAddressError("Change address not found in outputs") + + my_addr = get_primary_change_address(state) + if not addr_eq(my_addr, change_addr): + raise misc.TrezorChangeAddressError("Change address differs from ours") + + return True + + +def process_payment_id(state: State, tsx_data): + """ + Payment id -> extra + """ + if common.is_empty(tsx_data.payment_id): + return + + from apps.monero.xmr.sub import tsx_helper + from trezor import utils + + if len(tsx_data.payment_id) == 8: + view_key_pub_enc = tsx_helper.get_destination_view_key_pub( + tsx_data.outputs, state.change_address() + ) + if view_key_pub_enc == crypto.NULL_KEY_ENC: + raise ValueError( + "Destinations have to have exactly one output to support encrypted payment ids" + ) + + view_key_pub = crypto.decodepoint(view_key_pub_enc) + payment_id_encr = tsx_helper.encrypt_payment_id( + tsx_data.payment_id, view_key_pub, state.tx_priv + ) + + extra_nonce = payment_id_encr + extra_prefix = 1 + + elif len(tsx_data.payment_id) == 32: + extra_nonce = tsx_data.payment_id + extra_prefix = 0 + + else: + raise ValueError("Payment ID size invalid") + + lextra = len(extra_nonce) + if lextra >= 255: + raise ValueError("Nonce could be 255 bytes max") + + extra_buff = bytearray(3 + lextra) + extra_buff[0] = 2 + extra_buff[1] = lextra + 1 + extra_buff[2] = extra_prefix + utils.memcpy(extra_buff, 3, extra_nonce, 0, lextra) + state.tx.extra = extra_buff + + +async def compute_sec_keys(state: State, tsx_data): + """ + Generate master key H(TsxData || r) + :return: + """ + import protobuf + from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer + + writer = get_keccak_writer() + await protobuf.dump_message(writer, tsx_data) + writer.write(crypto.encodeint(state.tx_priv)) + + state.key_master = crypto.keccak_2hash( + writer.get_digest() + crypto.encodeint(crypto.random_scalar()) + ) + state.key_hmac = crypto.keccak_2hash(b"hmac" + state.key_master) + state.key_enc = crypto.keccak_2hash(b"enc" + state.key_master) + + +def precompute_subaddr(state, account, indices): + """ + Precomputes subaddresses for account (major) and list of indices (minors) + Subaddresses have to be stored in encoded form - unique representation. + Single point can have multiple extended coordinates representation - would not match during subaddress search. + """ + monero.compute_subaddresses(state.creds, account, indices, state.subaddresses) diff --git a/src/apps/monero/protocol/signing/step_02_set_input.py b/src/apps/monero/protocol/signing/step_02_set_input.py new file mode 100644 index 000000000..2eb9d94c0 --- /dev/null +++ b/src/apps/monero/protocol/signing/step_02_set_input.py @@ -0,0 +1,171 @@ +""" +Sets UTXO one by one. +Computes spending secret key, key image. tx.vin[i] + HMAC, Pedersen commitment on amount. + +If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are kept in the Trezor. +Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under AES-GCM() with +key derived for exactly this purpose. +""" + +from .state import State + +from apps.monero.controller import misc +from apps.monero.layout.confirms import transaction_step +from apps.monero.xmr import crypto, monero + + +async def set_input(state: State, src_entr): + """ + Sets UTXO one by one. + Computes spending secret key, key image. tx.vin[i] + HMAC, Pedersen commitment on amount. + + If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are kept in the Trezor. + Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under Chacha20Poly1305() + with key derived for exactly this purpose. + """ + from trezor.messages.MoneroTransactionSetInputAck import ( + MoneroTransactionSetInputAck + ) + from apps.monero.xmr.enc import chacha_poly + from apps.monero.xmr.sub import tsx_helper + from apps.monero.xmr.serialize_messages.tx_prefix import TxinToKey + from apps.monero.protocol import hmac_encryption_keys + + state.inp_idx += 1 + + await transaction_step(state.STEP_INP, state.inp_idx, state.num_inputs()) + + if state.inp_idx >= state.num_inputs(): + raise ValueError("Too many inputs") + if src_entr.real_output >= len(src_entr.outputs): + raise ValueError( + "real_output index %s bigger than output_keys.size() %s" + % (src_entr.real_output, len(src_entr.outputs)) + ) + state.summary_inputs_money += src_entr.amount + + # Secrets derivation + out_key = crypto.decodepoint(src_entr.outputs[src_entr.real_output].key.dest) + tx_key = crypto.decodepoint(src_entr.real_out_tx_key) + additional_keys = [ + crypto.decodepoint(x) for x in src_entr.real_out_additional_tx_keys + ] + + secs = monero.generate_tx_spend_and_key_image_and_derivation( + state.creds, + state.subaddresses, + out_key, + tx_key, + additional_keys, + src_entr.real_output_in_tx_index, + ) + xi, ki, di = secs + state._mem_trace(1, True) + + # Construct tx.vin + ki_real = src_entr.multisig_kLRki.ki if state.multi_sig else ki + vini = TxinToKey(amount=src_entr.amount, k_image=crypto.encodepoint(ki_real)) + vini.key_offsets = tsx_helper.absolute_output_offsets_to_relative( + [x.idx for x in src_entr.outputs] + ) + + if src_entr.rct: + vini.amount = 0 + + # Serialize with variant code for TxinToKey + vini_bin = misc.dump_msg(vini, preallocate=64, prefix=b"\x02") + state._mem_trace(2, True) + + # HMAC(T_in,i || vin_i) + hmac_vini = await hmac_encryption_keys.gen_hmac_vini( + state.key_hmac, src_entr, vini_bin, state.inp_idx + ) + state._mem_trace(3, True) + + # PseudoOuts commitment, alphas stored to state + pseudo_out = None + pseudo_out_hmac = None + alpha_enc = None + + if state.use_simple_rct: + alpha, pseudo_out = _gen_commitment(state, src_entr.amount) + pseudo_out = crypto.encodepoint(pseudo_out) + + # In full version the alpha is encrypted and passed back for storage + pseudo_out_hmac = crypto.compute_hmac( + hmac_encryption_keys.hmac_key_txin_comm(state.key_hmac, state.inp_idx), + pseudo_out, + ) + alpha_enc = chacha_poly.encrypt_pack( + hmac_encryption_keys.enc_key_txin_alpha(state.key_enc, state.inp_idx), + crypto.encodeint(alpha), + ) + + spend_enc = chacha_poly.encrypt_pack( + hmac_encryption_keys.enc_key_spend(state.key_enc, state.inp_idx), + crypto.encodeint(xi), + ) + + result_msg = MoneroTransactionSetInputAck( + vini=vini_bin, + vini_hmac=hmac_vini, + pseudo_out=pseudo_out, + pseudo_out_hmac=pseudo_out_hmac, + alpha_enc=alpha_enc, + spend_enc=spend_enc, + ) + + # All inputs done? + if state.inp_idx + 1 != state.num_inputs(): + return await dispatch_and_next_input( + state, result_msg + ) # todo: we can merge those two functions + + return await dispatch_and_forward(state, result_msg) + + +async def dispatch_and_next_input(state, result_msg): + from trezor.messages import MessageType + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read((MessageType.MoneroTransactionSetInputRequest,)) + return await set_input(state, received_msg.src_entr) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_03_inputs_permutation + + """ + All inputs set + """ + state.subaddresses = None # todo why? + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + (MessageType.MoneroTransactionInputsPermutationRequest,) + ) + + return await step_03_inputs_permutation.tsx_inputs_permutation( + state, received_msg.perm + ) + + +def _gen_commitment(state: State, in_amount): + """ + Computes Pedersen commitment - pseudo outs + Here is slight deviation from the original protocol. + We want that \\sum Alpha = \\sum A_{i,j} where A_{i,j} is a mask from range proof for output i, bit j. + + Previously this was computed in such a way that Alpha_{last} = \\sum A{i,j} - \\sum_{i=0}^{last-1} Alpha + But we would prefer to compute commitment before range proofs so alphas are generated completely randomly + and the last A mask is computed in this special way. + Returns pseudo_out + """ + alpha = crypto.random_scalar() + state.sumpouts_alphas = crypto.sc_add(state.sumpouts_alphas, alpha) + return alpha, crypto.gen_commitment(alpha, in_amount) diff --git a/src/apps/monero/protocol/signing/step_03_inputs_permutation.py b/src/apps/monero/protocol/signing/step_03_inputs_permutation.py new file mode 100644 index 000000000..735f5f9b3 --- /dev/null +++ b/src/apps/monero/protocol/signing/step_03_inputs_permutation.py @@ -0,0 +1,53 @@ +""" +Set permutation on the inputs - sorted by key image on host. +""" + +from .state import State + +from apps.monero.layout.confirms import transaction_step +from apps.monero.xmr import common + + +async def tsx_inputs_permutation(state: State, permutation): + """ + Set permutation on the inputs - sorted by key image on host. + """ + from trezor.messages.MoneroTransactionInputsPermutationAck import ( + MoneroTransactionInputsPermutationAck + ) + + await transaction_step(state.ctx, state.STEP_PERM) + + _tsx_inputs_permutation(state, permutation) + + return await dispatch_and_forward(state, MoneroTransactionInputsPermutationAck()) + + +def _tsx_inputs_permutation(state: State, permutation): + """ + Set permutation on the inputs - sorted by key image on host. + """ + state.source_permutation = permutation + common.check_permutation(permutation) + state.inp_idx = -1 + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_04_input_vini + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + (MessageType.MoneroTransactionInputViniRequest,) + ) + + return await step_04_input_vini.input_vini( + state, + received_msg.src_entr, + received_msg.vini, + received_msg.vini_hmac, + received_msg.pseudo_out, + received_msg.pseudo_out_hmac, + ) diff --git a/src/apps/monero/protocol/signing/step_04_input_vini.py b/src/apps/monero/protocol/signing/step_04_input_vini.py new file mode 100644 index 000000000..236ccd386 --- /dev/null +++ b/src/apps/monero/protocol/signing/step_04_input_vini.py @@ -0,0 +1,98 @@ +""" +Set tx.vin[i] for incremental tx prefix hash computation. +After sorting by key images on host. +Hashes pseudo_out to the final_message. +""" + +from .state import State + +from apps.monero.layout import confirms +from apps.monero.protocol import hmac_encryption_keys +from apps.monero.xmr import common, crypto, monero + + +async def input_vini( + state: State, + src_entr, + vini_bin, + hmac, + pseudo_out, + pseudo_out_hmac, +): + from trezor.messages.MoneroTransactionInputViniAck import ( + MoneroTransactionInputViniAck + ) + + await confirms.transaction_step( + state.ctx, state.STEP_VINI, state.inp_idx + 1, state.num_inputs() + ) + + if state.inp_idx >= state.num_inputs(): + raise ValueError("Too many inputs") + + state.inp_idx += 1 + + # HMAC(T_in,i || vin_i) + hmac_vini = await hmac_encryption_keys.gen_hmac_vini( + state.key_hmac, src_entr, vini_bin, state.source_permutation[state.inp_idx] + ) + if not common.ct_equal(hmac_vini, hmac): + raise ValueError("HMAC is not correct") + + hash_vini_pseudo_out(state, vini_bin, state.inp_idx, pseudo_out, pseudo_out_hmac) + + return await dispatch_and_forward(state, MoneroTransactionInputViniAck()) + + +def hash_vini_pseudo_out( + state: State, + vini_bin, + inp_idx, + pseudo_out=None, + pseudo_out_hmac=None, +): + """ + Incremental hasing of tx.vin[i] and pseudo output + """ + state.tx_prefix_hasher.buffer(vini_bin) + + # Pseudo_out incremental hashing - applicable only in simple rct + if not state.use_simple_rct or state.use_bulletproof: + return + + idx = state.source_permutation[inp_idx] + pseudo_out_hmac_comp = crypto.compute_hmac( + hmac_encryption_keys.hmac_key_txin_comm(state.key_hmac, idx), pseudo_out + ) + if not common.ct_equal(pseudo_out_hmac, pseudo_out_hmac_comp): + raise ValueError("HMAC invalid for pseudo outs") + + state.full_message_hasher.set_pseudo_out(pseudo_out) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_05_all_in_set + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + ( + MessageType.MoneroTransactionInputViniRequest, + MessageType.MoneroTransactionAllInputsSetRequest, + ) + ) + # todo check input count + + if received_msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionInputViniRequest: + return await input_vini( + state, + received_msg.src_entr, + received_msg.vini, + received_msg.vini_hmac, + received_msg.pseudo_out, + received_msg.pseudo_out_hmac, + ) + + return await step_05_all_in_set.all_in_set(state, received_msg.rsig_data) diff --git a/src/apps/monero/protocol/signing/step_05_all_in_set.py b/src/apps/monero/protocol/signing/step_05_all_in_set.py new file mode 100644 index 000000000..9da137aff --- /dev/null +++ b/src/apps/monero/protocol/signing/step_05_all_in_set.py @@ -0,0 +1,66 @@ +""" +All inputs set. Defining rsig parameters. +""" + +from trezor import utils + +from .state import State + +from apps.monero.layout import confirms +from apps.monero.xmr import crypto + + +async def all_in_set(state: State, rsig_data): # todo: rsig_data not used? + """ + If in the applicable offloading mode, generate commitment masks. + """ + state._mem_trace(0) + # state.state.input_all_done() todo check if needed? + await confirms.transaction_step(state.ctx, state.STEP_ALL_IN) + + from trezor.messages.MoneroTransactionAllInputsSetAck import ( + MoneroTransactionAllInputsSetAck + ) + from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData + + rsig_data = MoneroTransactionRsigData() + resp = MoneroTransactionAllInputsSetAck(rsig_data=rsig_data) + + if not state.rsig_offload: + return await dispatch_and_forward(state, resp) + + # Simple offloading - generate random masks that sum to the input mask sum. + tmp_buff = bytearray(32) + rsig_data.mask = bytearray(32 * state.num_dests()) + state.sumout = crypto.sc_init(0) + for i in range(state.num_dests()): + cur_mask = crypto.new_scalar() + is_last = i + 1 == state.num_dests() + if is_last and state.use_simple_rct: + crypto.sc_sub_into(cur_mask, state.sumpouts_alphas, state.sumout) + else: + crypto.random_scalar(cur_mask) + + crypto.sc_add_into(state.sumout, state.sumout, cur_mask) + state.output_masks.append(cur_mask) + crypto.encodeint_into(tmp_buff, cur_mask) + utils.memcpy(rsig_data.mask, 32 * i, tmp_buff, 0, 32) + + state.assrt(crypto.sc_eq(state.sumout, state.sumpouts_alphas), "Invalid masks sum") + state.sumout = crypto.sc_init(0) + return await dispatch_and_forward(state, resp) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_06_set_out1 + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + (MessageType.MoneroTransactionSetOutputRequest,) + ) + return await step_06_set_out1.set_out1( + state, received_msg.dst_entr, received_msg.dst_entr_hmac, received_msg.rsig_data + ) diff --git a/src/apps/monero/protocol/signing/step_06_set_out1.py b/src/apps/monero/protocol/signing/step_06_set_out1.py new file mode 100644 index 000000000..8db2ba629 --- /dev/null +++ b/src/apps/monero/protocol/signing/step_06_set_out1.py @@ -0,0 +1,386 @@ +""" +Set destination entry one by one. +Computes destination stealth address, amount key, range proof + HMAC, out_pk, ecdh_info. +""" +import gc + +from trezor import utils + +from .state import State + +from apps.monero.controller import misc +from apps.monero.layout import confirms +from apps.monero.protocol import hmac_encryption_keys +from apps.monero.xmr import common, crypto + + +async def set_out1( + state: State, dst_entr, dst_entr_hmac, rsig_data=None +): + state._mem_trace(0, True) + mods = utils.unimport_begin() + + await confirms.transaction_step( + state.ctx, state.STEP_OUT, state.out_idx + 1, state.num_dests() + ) + state._mem_trace(1) + + if ( + state.inp_idx + 1 != state.num_inputs() + ): # todo check state.state.is_input_vins() - needed? + raise ValueError("Invalid number of inputs") + + state.out_idx += 1 + state._mem_trace(2, True) + + if dst_entr.amount <= 0 and state.tx.version <= 1: + raise ValueError("Destination with wrong amount: %s" % dst_entr.amount) + + # HMAC check of the destination + dst_entr_hmac_computed = await hmac_encryption_keys.gen_hmac_tsxdest( + state.key_hmac, dst_entr, state.out_idx + ) + if not common.ct_equal(dst_entr_hmac, dst_entr_hmac_computed): + raise ValueError("HMAC invalid") + del (dst_entr_hmac, dst_entr_hmac_computed) + state._mem_trace(3, True) + + # First output - tx prefix hasher - size of the container + if state.out_idx == 0: + state.tx_prefix_hasher.container_size(state.num_dests()) + state._mem_trace(4, True) + + state.summary_outs_money += dst_entr.amount + utils.unimport_end(mods) + state._mem_trace(5, True) + + # Range proof first, memory intensive + rsig, mask = _range_proof(state, state.out_idx, dst_entr.amount, rsig_data) + utils.unimport_end(mods) + state._mem_trace(6, True) + + # Amount key, tx out key + additional_txkey_priv = _set_out1_additional_keys(state, dst_entr) + derivation = _set_out1_derivation(state, dst_entr, additional_txkey_priv) + amount_key = crypto.derivation_to_scalar(derivation, state.out_idx) + tx_out_key = crypto.derive_public_key( + derivation, state.out_idx, crypto.decodepoint(dst_entr.addr.spend_public_key) + ) + del (derivation, additional_txkey_priv) + state._mem_trace(7, True) + + # Tx header prefix hashing, hmac dst_entr + tx_out_bin, hmac_vouti = await _set_out1_tx_out(state, dst_entr, tx_out_key) + state._mem_trace(11, True) + + # Out_pk, ecdh_info + out_pk, ecdh_info_bin = _set_out1_ecdh( + state=state, + dest_pub_key=tx_out_key, + amount=dst_entr.amount, + mask=mask, + amount_key=amount_key, + ) + del (dst_entr, mask, amount_key, tx_out_key) + state._mem_trace(12, True) + + # Incremental hashing of the ECDH info. + # RctSigBase allows to hash only one of the (ecdh, out_pk) as they are serialized + # as whole vectors. Hashing ECDH info saves state space. + state.full_message_hasher.set_ecdh(ecdh_info_bin) + state._mem_trace(13, True) + + # Output_pk is stored to the state as it is used during the signature and hashed to the + # RctSigBase later. + state.output_pk.append(out_pk) + state._mem_trace(14, True) + + from trezor.messages.MoneroTransactionSetOutputAck import ( + MoneroTransactionSetOutputAck + ) + + out_pk_bin = bytearray(64) + utils.memcpy(out_pk_bin, 0, out_pk.dest, 0, 32) + utils.memcpy(out_pk_bin, 32, out_pk.mask, 0, 32) + + result = MoneroTransactionSetOutputAck( + tx_out=tx_out_bin, + vouti_hmac=hmac_vouti, + rsig_data=_return_rsig_data(state, rsig), + out_pk=out_pk_bin, + ecdh_info=ecdh_info_bin, + ) + return await dispatch_and_forward(state, result) + + +async def _set_out1_tx_out(state: State, dst_entr, tx_out_key): + # Manual serialization of TxOut(0, TxoutToKey(key)) + tx_out_bin = bytearray(34) + tx_out_bin[0] = 0 # amount varint + tx_out_bin[1] = 2 # variant code TxoutToKey + crypto.encodepoint_into(tx_out_bin, tx_out_key, 2) + state._mem_trace(8) + + # Tx header prefix hashing + state.tx_prefix_hasher.buffer(tx_out_bin) + state._mem_trace(9, True) + + # Hmac dest_entr. + hmac_vouti = await hmac_encryption_keys.gen_hmac_vouti( + state.key_hmac, dst_entr, tx_out_bin, state.out_idx + ) + state._mem_trace(10, True) + return tx_out_bin, hmac_vouti + + +def _range_proof(state, idx, amount, rsig_data=None): + """ + Computes rangeproof and related information - out_sk, out_pk, ecdh_info. + In order to optimize incremental transaction build, the mask computation is changed compared + to the official Monero code. In the official code, the input pedersen commitments are computed + after range proof in such a way summed masks for commitments (alpha) and rangeproofs (ai) are equal. + + In order to save roundtrips we compute commitments randomly and then for the last rangeproof + a[63] = (\\sum_{i=0}^{num_inp}alpha_i - \\sum_{i=0}^{num_outs-1} amasks_i) - \\sum_{i=0}^{62}a_i + + The range proof is incrementally hashed to the final_message. + """ + from apps.monero.xmr import ring_ct + + mask = _get_out_mask(state, idx) + state.output_amounts.append(amount) + provided_rsig = ( + rsig_data.rsig + if rsig_data and rsig_data.rsig and len(rsig_data.rsig) > 0 + else None + ) + if not state.rsig_offload and provided_rsig: + raise misc.TrezorError("Provided unexpected rsig") + if not state.rsig_offload: + state.output_masks.append(mask) + + # Batching + bidx = _get_rsig_batch(state, idx) + batch_size = state.rsig_grp[bidx] + last_in_batch = _is_last_in_batch(state, idx, bidx) + if state.rsig_offload and provided_rsig and not last_in_batch: + raise misc.TrezorError("Provided rsig too early") + if state.rsig_offload and last_in_batch and not provided_rsig: + raise misc.TrezorError("Rsig expected, not provided") + + # Batch not finished, skip range sig generation now + if not last_in_batch: + return None, mask + + # Rangeproof + # Pedersen commitment on the value, mask from the commitment, range signature. + C, rsig = None, None + + state._mem_trace("pre-rproof" if __debug__ else None, collect=True) + if not state.rsig_offload and state.use_bulletproof: + rsig = ring_ct.prove_range_bp_batch(state.output_amounts, state.output_masks) + state._mem_trace("post-bp" if __debug__ else None, collect=True) + + # Incremental hashing + state.full_message_hasher.rsig_val(rsig, True, raw=False) + state._mem_trace("post-bp-hash" if __debug__ else None, collect=True) + + rsig = misc.dump_rsig_bp(rsig) + state._mem_trace( + "post-bp-ser, size: %s" % len(rsig) if __debug__ else None, collect=True + ) + + elif not state.rsig_offload and not state.use_bulletproof: + C, mask, rsig = ring_ct.prove_range_chunked(amount, mask) + del (ring_ct) + + # Incremental hashing + state.full_message_hasher.rsig_val(rsig, False, raw=True) + _check_out_commitment(state, amount, mask, C) + + elif state.rsig_offload and state.use_bulletproof: + from apps.monero.xmr.serialize_messages.tx_rsig_bulletproof import Bulletproof + + masks = [ + _get_out_mask(state, 1 + idx - batch_size + ix) for ix in range(batch_size) + ] + + bp_obj = misc.parse_msg(rsig_data.rsig, Bulletproof()) + rsig_data.rsig = None + + state.full_message_hasher.rsig_val(bp_obj, True, raw=False) + res = ring_ct.verify_bp(bp_obj, state.output_amounts, masks) + state.assrt(res, "BP verification fail") + state._mem_trace("BP verified" if __debug__ else None, collect=True) + del (bp_obj, ring_ct) + + elif state.rsig_offload and not state.use_bulletproof: + state.full_message_hasher.rsig_val(rsig_data.rsig, False, raw=True) + rsig_data.rsig = None + + else: + raise misc.TrezorError("Unexpected rsig state") + + state._mem_trace("rproof" if __debug__ else None, collect=True) + state.output_amounts = [] + if not state.rsig_offload: + state.output_masks = [] + return rsig, mask + + +def _return_rsig_data(state, rsig): + if rsig is None: + return None + from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData + + if isinstance(rsig, list): + return MoneroTransactionRsigData(rsig_parts=rsig) + else: + return MoneroTransactionRsigData(rsig=rsig) + + +def _set_out1_ecdh( + state: State, dest_pub_key, amount, mask, amount_key +): + from apps.monero.xmr import ring_ct + + # Mask sum + out_pk = misc.StdObj( + dest=crypto.encodepoint(dest_pub_key), + mask=crypto.encodepoint(crypto.gen_commitment(mask, amount)), + ) + state.sumout = crypto.sc_add(state.sumout, mask) + state.output_sk.append(misc.StdObj(mask=mask)) + + # ECDH masking + from apps.monero.xmr.sub.recode import recode_ecdh + + ecdh_info = misc.StdObj(mask=mask, amount=crypto.sc_init(amount)) + ring_ct.ecdh_encode_into( + ecdh_info, ecdh_info, derivation=crypto.encodeint(amount_key) + ) + recode_ecdh(ecdh_info, encode=True) + + ecdh_info_bin = bytearray(64) + utils.memcpy(ecdh_info_bin, 0, ecdh_info.mask, 0, 32) + utils.memcpy(ecdh_info_bin, 32, ecdh_info.amount, 0, 32) + gc.collect() + + return out_pk, ecdh_info_bin + + +def _set_out1_additional_keys(state: State, dst_entr): + additional_txkey = None + additional_txkey_priv = None + if state.need_additional_txkeys: + use_provided = state.num_dests() == len(state.additional_tx_private_keys) + additional_txkey_priv = ( + state.additional_tx_private_keys[state.out_idx] + if use_provided + else crypto.random_scalar() + ) + + if dst_entr.is_subaddress: + additional_txkey = crypto.scalarmult( + crypto.decodepoint(dst_entr.addr.spend_public_key), + additional_txkey_priv, + ) + else: + additional_txkey = crypto.scalarmult_base(additional_txkey_priv) + + state.additional_tx_public_keys.append(crypto.encodepoint(additional_txkey)) + if not use_provided: + state.additional_tx_private_keys.append(additional_txkey_priv) + return additional_txkey_priv + + +def _set_out1_derivation( + state: State, dst_entr, additional_txkey_priv +): + from apps.monero.xmr.sub.addr import addr_eq + + change_addr = state.change_address() + if change_addr and addr_eq(dst_entr.addr, change_addr): + # sending change to yourself; derivation = a*R + derivation = crypto.generate_key_derivation( + state.tx_pub, state.creds.view_key_private + ) + + else: + # sending to the recipient; derivation = r*A (or s*C in the subaddress scheme) + deriv_priv = ( + additional_txkey_priv + if dst_entr.is_subaddress and state.need_additional_txkeys + else state.tx_priv + ) + derivation = crypto.generate_key_derivation( + crypto.decodepoint(dst_entr.addr.view_public_key), deriv_priv + ) + return derivation + + +def _check_out_commitment(state: State, amount, mask, C): + state.assrt( + crypto.point_eq( + C, + crypto.point_add(crypto.scalarmult_base(mask), crypto.scalarmult_h(amount)), + ), + "OutC fail", + ) + + +def _is_last_in_batch(state: State, idx, bidx=None): + """ + Returns true if the current output is last in the rsig batch + """ + bidx = _get_rsig_batch(idx) if bidx is None else bidx + batch_size = state.rsig_grp[bidx] + return (idx - sum(state.rsig_grp[:bidx])) + 1 == batch_size + + +def _get_rsig_batch(state: State, idx): + """ + Returns index of the current rsig batch + """ + r = 0 + c = 0 + while c < idx + 1: + c += state.rsig_grp[r] + r += 1 + return r - 1 + + +def _get_out_mask(state: State, idx): + if state.rsig_offload: + return state.output_masks[idx] + else: + is_last = idx + 1 == state.num_dests() + if is_last: + return crypto.sc_sub(state.sumpouts_alphas, state.sumout) + else: + return crypto.random_scalar() + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_07_all_out1_set + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + ( + MessageType.MoneroTransactionSetOutputRequest, + MessageType.MoneroTransactionAllOutSetRequest, + ) + ) + + if received_msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionSetOutputRequest: + return await set_out1( + state, + received_msg.dst_entr, + received_msg.dst_entr_hmac, + received_msg.rsig_data, + ) + + return await step_07_all_out1_set.all_out1_set(state) diff --git a/src/apps/monero/protocol/signing/step_07_all_out1_set.py b/src/apps/monero/protocol/signing/step_07_all_out1_set.py new file mode 100644 index 000000000..c807cd6a9 --- /dev/null +++ b/src/apps/monero/protocol/signing/step_07_all_out1_set.py @@ -0,0 +1,127 @@ +""" +All outputs were set in this phase. Computes additional public keys (if needed), tx.extra and +transaction prefix hash. +Adds additional public keys to the tx.extra +""" + +import gc + +from .state import State + +from apps.monero.controller import misc +from apps.monero.layout import confirms +from apps.monero.xmr import common, crypto + + +async def all_out1_set(state: State): + print("07") + + state._mem_trace(0) + # state.state.set_output_done() todo needed? + + await confirms.transaction_step(state.ctx, state.STEP_ALL_OUT) + state._mem_trace(1) + + if state.out_idx + 1 != state.num_dests(): + raise ValueError("Invalid out num") + + # Test if \sum Alpha == \sum A + if state.use_simple_rct: + state.assrt(crypto.sc_eq(state.sumout, state.sumpouts_alphas)) + + # Fee test + if state.fee != (state.summary_inputs_money - state.summary_outs_money): + raise ValueError( + "Fee invalid %s vs %s, out: %s" + % ( + state.fee, + state.summary_inputs_money - state.summary_outs_money, + state.summary_outs_money, + ) + ) + state._mem_trace(2) + + # Set public key to the extra + # Not needed to remove - extra is clean + _set_tx_extra(state) + state.additional_tx_public_keys = None + + gc.collect() + state._mem_trace(3) + + if state.summary_outs_money > state.summary_inputs_money: + raise ValueError( + "Transaction inputs money (%s) less than outputs money (%s)" + % (state.summary_inputs_money, state.summary_outs_money) + ) + + # Hashing transaction prefix + _set_tx_prefix(state) + extra_b = state.tx.extra + state.tx = None + gc.collect() + state._mem_trace(4) + + # Txprefix match check for multisig + if not common.is_empty(state.exp_tx_prefix_hash) and not common.ct_equal( + state.exp_tx_prefix_hash, state.tx_prefix_hash + ): + # state.state.set_fail() todo needed? + # todo raise wire.NotEnoughFunds(e.message) ?? + raise misc.TrezorTxPrefixHashNotMatchingError("Tx prefix invalid") + + gc.collect() + state._mem_trace(5) + + from trezor.messages.MoneroRingCtSig import MoneroRingCtSig + from trezor.messages.MoneroTransactionAllOutSetAck import ( + MoneroTransactionAllOutSetAck + ) + + # Initializes RCTsig structure (fee, tx prefix hash, type) + rv_pb = MoneroRingCtSig( + txn_fee=state.get_fee(), + message=state.tx_prefix_hash, + rv_type=state.get_rct_type(), + ) + + result = MoneroTransactionAllOutSetAck( + extra=extra_b, tx_prefix_hash=state.tx_prefix_hash, rv=rv_pb + ) + return await dispatch_and_forward(state, result) + + +def _set_tx_extra(state: State): + from apps.monero.xmr.sub import tsx_helper + + state.tx.extra = tsx_helper.add_tx_pub_key_to_extra(state.tx.extra, state.tx_pub) + + # Not needed to remove - extra is clean + # state.tx.extra = await monero.remove_field_from_tx_extra(state.tx.extra, xmrtypes.TxExtraAdditionalPubKeys) + if state.need_additional_txkeys: + state.tx.extra = tsx_helper.add_additional_tx_pub_keys_to_extra( + state.tx.extra, pub_enc=state.additional_tx_public_keys + ) + + +def _set_tx_prefix(state: State): + from apps.monero.xmr.serialize.message_types import BlobType + + state.tx_prefix_hasher.message_field(state.tx, ("extra", BlobType)) # extra + + state.tx_prefix_hash = state.tx_prefix_hasher.get_digest() + state.tx_prefix_hasher = None + + # Hash message to the final_message + state.full_message_hasher.set_message(state.tx_prefix_hash) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_08_mlsag_done + + await state.ctx.write(result_msg) + del result_msg + + await state.ctx.read((MessageType.MoneroTransactionMlsagDoneRequest,)) + return await step_08_mlsag_done.mlsag_done(state) diff --git a/src/apps/monero/protocol/signing/step_08_mlsag_done.py b/src/apps/monero/protocol/signing/step_08_mlsag_done.py new file mode 100644 index 000000000..cc66a2d6f --- /dev/null +++ b/src/apps/monero/protocol/signing/step_08_mlsag_done.py @@ -0,0 +1,67 @@ +""" +MLSAG message computed. +""" + +from .state import State + +from apps.monero.layout import confirms + + +async def mlsag_done(state: State): + from trezor.messages.MoneroTransactionMlsagDoneAck import ( + MoneroTransactionMlsagDoneAck + ) + # state.state.set_final_message_done() todo needed? + await confirms.transaction_step(state.ctx, state.STEP_MLSAG) + + _ecdh_info(state) + _out_pk(state) + state.full_message_hasher.rctsig_base_done() + state.out_idx = -1 + state.inp_idx = -1 + + state.full_message = state.full_message_hasher.get_digest() + state.full_message_hasher = None + + result = MoneroTransactionMlsagDoneAck(full_message_hash=state.full_message) + return await dispatch_and_forward(state, result) + + +def _ecdh_info(state: State): # todo why is it here? remove + """ + Sets ecdh info for the incremental hashing mlsag. + """ + pass + + +def _out_pk(state: State): + """ + Sets out_pk for the incremental hashing mlsag. + """ + if state.num_dests() != len(state.output_pk): + raise ValueError("Invalid number of ecdh") + + for out in state.output_pk: + state.full_message_hasher.set_out_pk(out) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_09_sign_input + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + (MessageType.MoneroTransactionSignInputRequest,) + ) + return await step_09_sign_input.sign_input( + state, + received_msg.src_entr, + received_msg.vini, + received_msg.vini_hmac, + received_msg.pseudo_out, + received_msg.pseudo_out_hmac, + received_msg.alpha_enc, + received_msg.spend_enc, + ) diff --git a/src/apps/monero/protocol/signing/step_09_sign_input.py b/src/apps/monero/protocol/signing/step_09_sign_input.py new file mode 100644 index 000000000..27cebc6c1 --- /dev/null +++ b/src/apps/monero/protocol/signing/step_09_sign_input.py @@ -0,0 +1,230 @@ +""" +Generates a signature for one input. +""" + +import gc + +from .state import State + +from apps.monero.controller import misc +from apps.monero.xmr import common, crypto +from apps.monero.layout import confirms + + +async def sign_input( + state: State, + src_entr, + vini_bin, + hmac_vini, + pseudo_out, + pseudo_out_hmac, + alpha_enc, + spend_enc, +): + """ + :param state: transaction state + :param src_entr: Source entry + :param vini_bin: tx.vin[i] for the transaction. Contains key image, offsets, amount (usually zero) + :param hmac_vini: HMAC for the tx.vin[i] as returned from Trezor + :param pseudo_out: pedersen commitment for the current input, uses alpha as the mask. + Only in memory offloaded scenario. Tuple containing HMAC, as returned from the Trezor. + :param pseudo_out_hmac: + :param alpha_enc: alpha mask for the current input. Only in memory offloaded scenario, + tuple as returned from the Trezor + :param spend_enc: + :return: Generated signature MGs[i] + """ + from apps.monero.protocol import hmac_encryption_keys + + # state.state.set_signature() todo + print("09") + await confirms.transaction_step( + state.ctx, state.STEP_SIGN, state.inp_idx + 1, state.num_inputs() + ) + + state.inp_idx += 1 + if state.inp_idx >= state.num_inputs(): + raise ValueError("Invalid ins") + if state.use_simple_rct and alpha_enc is None: + raise ValueError("Inconsistent1") + if state.use_simple_rct and pseudo_out is None: + raise ValueError("Inconsistent2") + if state.inp_idx >= 1 and not state.use_simple_rct: + raise ValueError("Inconsistent3") + + inv_idx = state.source_permutation[state.inp_idx] + + # Check HMAC of all inputs + hmac_vini_comp = await hmac_encryption_keys.gen_hmac_vini( + state.key_hmac, src_entr, vini_bin, inv_idx + ) + if not common.ct_equal(hmac_vini_comp, hmac_vini): + raise ValueError("HMAC is not correct") + + gc.collect() + state._mem_trace(1) + + if state.use_simple_rct: + pseudo_out_hmac_comp = crypto.compute_hmac( + hmac_encryption_keys.hmac_key_txin_comm(state.key_hmac, inv_idx), pseudo_out + ) + if not common.ct_equal(pseudo_out_hmac_comp, pseudo_out_hmac): + raise ValueError("HMAC is not correct") + + gc.collect() + state._mem_trace(2) + + from apps.monero.xmr.enc import chacha_poly + + alpha_c = crypto.decodeint( + chacha_poly.decrypt_pack( + hmac_encryption_keys.enc_key_txin_alpha(state.key_enc, inv_idx), + bytes(alpha_enc), + ) + ) + pseudo_out_c = crypto.decodepoint(pseudo_out) + + elif state.use_simple_rct: + alpha_c = state.input_alphas[state.inp_idx] + pseudo_out_c = crypto.decodepoint(state.input_pseudo_outs[state.inp_idx]) + + else: + alpha_c = None + pseudo_out_c = None + + # Spending secret + from apps.monero.xmr.enc import chacha_poly + + input_secret = crypto.decodeint( + chacha_poly.decrypt_pack( + hmac_encryption_keys.enc_key_spend(state.key_enc, inv_idx), bytes(spend_enc) + ) + ) + + gc.collect() + state._mem_trace(3) + + # Basic setup, sanity check + index = src_entr.real_output + in_sk = misc.StdObj(dest=input_secret, mask=crypto.decodeint(src_entr.mask)) + kLRki = src_entr.multisig_kLRki if state.multi_sig else None + + # Private key correctness test + state.assrt( + crypto.point_eq( + crypto.decodepoint(src_entr.outputs[src_entr.real_output].key.dest), + crypto.scalarmult_base(in_sk.dest), + ), + "a1", + ) + state.assrt( + crypto.point_eq( + crypto.decodepoint(src_entr.outputs[src_entr.real_output].key.mask), + crypto.gen_commitment(in_sk.mask, src_entr.amount), + ), + "a2", + ) + + gc.collect() + state._mem_trace(4) + + # RCT signature + gc.collect() + from apps.monero.xmr import mlsag2 + + if state.use_simple_rct: + # Simple RingCT + mix_ring = [x.key for x in src_entr.outputs] + mg, msc = mlsag2.prove_rct_mg_simple( + state.full_message, + mix_ring, + in_sk, + alpha_c, + pseudo_out_c, + kLRki, + None, + index, + ) + + else: + # Full RingCt, only one input + txn_fee_key = crypto.scalarmult_h(state.get_fee()) + mix_ring = [[x.key] for x in src_entr.outputs] + + mg, msc = mlsag2.prove_rct_mg( + state.full_message, + mix_ring, + [in_sk], + state.output_sk, + state.output_pk, + kLRki, + None, + index, + txn_fee_key, + ) + + gc.collect() + state._mem_trace(5) + + # Encode + from apps.monero.xmr.sub.recode import recode_msg + + mgs = recode_msg([mg]) + cout = None + + gc.collect() + state._mem_trace(6) + + # Multisig values returned encrypted, keys returned after finished successfully. + if state.multi_sig: + from apps.monero.xmr.enc import chacha_poly + + cout = chacha_poly.encrypt_pack( + hmac_encryption_keys.enc_key_cout(state.key_enc), crypto.encodeint(msc) + ) + + # Final state transition + if state.inp_idx + 1 == state.num_inputs(): + # state.state.set_signature_done() todo remove? + await confirms.transaction_signed(state.ctx) + + gc.collect() + state._mem_trace() + + from trezor.messages.MoneroTransactionSignInputAck import ( + MoneroTransactionSignInputAck + ) + + result = MoneroTransactionSignInputAck( + signature=misc.dump_msg_gc(mgs[0], preallocate=488, del_msg=True), cout=cout + ) + return await dispatch_and_forward(state, result) + + +async def dispatch_and_forward(state, result_msg): + from trezor.messages import MessageType + from apps.monero.protocol.signing import step_10_sign_final + + await state.ctx.write(result_msg) + del result_msg + + received_msg = await state.ctx.read( + ( + MessageType.MoneroTransactionSignInputRequest, + MessageType.MoneroTransactionFinalRequest, + ) + ) + + if received_msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionSignInputRequest: + return await sign_input( + state, + received_msg.src_entr, + received_msg.vini, + received_msg.vini_hmac, + received_msg.pseudo_out, + received_msg.pseudo_out_hmac, + received_msg.alpha_enc, + received_msg.spend_enc, + ) + + return await step_10_sign_final.final_msg(state) diff --git a/src/apps/monero/protocol/signing/step_10_sign_final.py b/src/apps/monero/protocol/signing/step_10_sign_final.py new file mode 100644 index 000000000..30c71d40e --- /dev/null +++ b/src/apps/monero/protocol/signing/step_10_sign_final.py @@ -0,0 +1,45 @@ +""" +Final message. +Offloading tx related data, encrypted. +""" + +import gc + +from .state import State + +from apps.monero.controller import misc +from apps.monero.xmr import crypto +from apps.monero.xmr.enc import chacha_poly +from apps.monero.protocol import hmac_encryption_keys +from trezor.messages.MoneroTransactionFinalAck import MoneroTransactionFinalAck + +from apps.monero.layout import confirms + + +async def final_msg(state: State): + # state.state.set_final() todo needed? + print("10") + + cout_key = ( + hmac_encryption_keys.enc_key_cout(state.key_enc) if state.multi_sig else None + ) + + # Encrypted tx keys under transaction specific key, derived from txhash and spend key. + # Deterministic transaction key, so we can recover it just from transaction and the spend key. + tx_key, salt, rand_mult = misc.compute_tx_key( + state.creds.spend_key_private, state.tx_prefix_hash + ) + + key_buff = crypto.encodeint(state.tx_priv) + b"".join( + [crypto.encodeint(x) for x in state.additional_tx_private_keys] + ) + tx_enc_keys = chacha_poly.encrypt_pack(tx_key, key_buff) + + await confirms.transaction_finished(state.ctx) + gc.collect() + + del state # todo ? + + return MoneroTransactionFinalAck( + cout_key=cout_key, salt=salt, rand_mult=rand_mult, tx_enc_keys=tx_enc_keys + ) diff --git a/src/apps/monero/protocol/tsx_sign_builder.py b/src/apps/monero/protocol/tsx_sign_builder.py deleted file mode 100644 index e356b6c46..000000000 --- a/src/apps/monero/protocol/tsx_sign_builder.py +++ /dev/null @@ -1,1310 +0,0 @@ -import gc -from micropython import const - -from trezor import log, utils - -from . import hmac_encryption_keys as keys - -from apps.monero.controller import misc -from apps.monero.layout import confirms -from apps.monero.xmr import common, crypto, monero - - -class TprefixStub: - __slots__ = ("version", "unlock_time", "vin", "vout", "extra") - - def __init__(self, **kwargs): - for kw in kwargs: - setattr(self, kw, kwargs[kw]) - - -class TTransactionBuilder: - """ - Transaction builder - """ - - STEP_INP = const(100) - STEP_PERM = const(200) - STEP_VINI = const(300) - STEP_ALL_IN = const(350) - STEP_OUT = const(400) - STEP_ALL_OUT = const(500) - STEP_MLSAG = const(600) - STEP_SIGN = const(700) - - def __init__(self, ctx, creds=None): - self.ctx = ctx - self.creds = creds - self.key_master = None - self.key_hmac = None - self.key_enc = None - - self.tx_priv = None # txkey - self.tx_pub = None - self.state = None - - self.multi_sig = False - self.need_additional_txkeys = False - self.use_bulletproof = False - self.use_rct = True - self.use_simple_rct = False - self.input_count = 0 - self.output_count = 0 - self.output_change = None - self.mixin = 0 - self.fee = 0 - self.account_idx = 0 - - self.additional_tx_private_keys = [] - self.additional_tx_public_keys = [] - self.inp_idx = -1 - self.out_idx = -1 - self.summary_inputs_money = 0 - self.summary_outs_money = 0 - self.input_secrets = [] - self.input_alphas = [] - self.input_pseudo_outs = [] - self.output_sk = [] - self.output_pk = [] - self.output_amounts = [] - self.output_masks = [] - self.rsig_type = 0 - self.rsig_grp = [] - self.rsig_offload = 0 - self.sumout = crypto.sc_0() - self.sumpouts_alphas = crypto.sc_0() - self.subaddresses = {} - self.tx = None - self.source_permutation = [] # sorted by key images - self.tx_prefix_hasher = None - self.tx_prefix_hash = None - self.full_message_hasher = None - self.full_message = None - self.exp_tx_prefix_hash = None - self._init() - - def _init(self): - from apps.monero.xmr.sub.keccak_hasher import KeccakXmrArchive - from apps.monero.xmr.sub.mlsag_hasher import PreMlsagHasher - from apps.monero.protocol.tsx_sign_state import TState - - self.state = TState() - self.tx = TprefixStub(vin=[], vout=[], extra=b"") - self.tx_prefix_hasher = KeccakXmrArchive() - self.full_message_hasher = PreMlsagHasher() - - def _mem_trace(self, x=None, collect=False): - if __debug__: - log.debug( - __name__, - "Log trace: %s, ... F: %s A: %s", - x, - gc.mem_free(), - gc.mem_alloc(), - ) - if collect: - gc.collect() - - def assrt(self, condition, msg=None): - if condition: - return - raise ValueError("Assertion error%s" % (" : %s" % msg if msg else "")) - - def is_terminal(self): - return self.state.is_terminal() - - def gen_r(self, use_r=None): - """ - Generates a new transaction key pair. - """ - self.tx_priv = crypto.random_scalar() if use_r is None else use_r - self.tx_pub = crypto.scalarmult_base(self.tx_priv) - - def get_primary_change_address(self): - """ - Computes primary change address for the current account index - """ - D, C = monero.generate_sub_address_keys( - self.creds.view_key_private, - self.creds.spend_key_public, - self.account_idx, - 0, - ) - return misc.StdObj( - view_public_key=crypto.encodepoint(C), - spend_public_key=crypto.encodepoint(D), - ) - - def check_change(self, outputs): - """ - Checks if the change address is among tx outputs and it is equal to our address. - """ - from apps.monero.xmr.sub.addr import addr_eq, get_change_addr_idx - - change_idx = get_change_addr_idx(outputs, self.output_change) - - change_addr = self.change_address() - if change_addr is None: - self._mem_trace("No change" if __debug__ else None) - return - - if change_idx is None and self.output_change.amount == 0 and len(outputs) == 2: - self._mem_trace("Sweep tsx" if __debug__ else None) - return # sweep dummy tsx - - found = False - for out in outputs: - if addr_eq(out.addr, change_addr): - found = True - break - - if not found: - raise misc.TrezorChangeAddressError("Change address not found in outputs") - - my_addr = self.get_primary_change_address() - if not addr_eq(my_addr, change_addr): - raise misc.TrezorChangeAddressError("Change address differs from ours") - - return True - - def num_inputs(self): - return self.input_count - - def num_dests(self): - return self.output_count - - def get_fee(self): - return self.fee if self.fee > 0 else 0 - - def change_address(self): - return self.output_change.addr if self.output_change else None - - def get_rct_type(self): - """ - RCTsig type (simple/full x Borromean/Bulletproof) - :return: - """ - from apps.monero.xmr.serialize_messages.tx_rsig import RctType - - if self.use_simple_rct: - return RctType.FullBulletproof if self.use_bulletproof else RctType.Simple - else: - return RctType.Full - - def init_rct_sig(self): - """ - Initializes RCTsig structure (fee, tx prefix hash, type) - """ - rv = misc.StdObj( - txnFee=self.get_fee(), message=self.tx_prefix_hash, type=self.get_rct_type() - ) - return rv - - async def init_transaction(self, tsx_data): - """ - Initializes a new transaction. - """ - from apps.monero.xmr.sub.addr import classify_subaddresses - - # Generate new TX key - self.tx_priv = crypto.random_scalar() - self.tx_pub = crypto.scalarmult_base(self.tx_priv) - - self.state.init_tsx() - self._mem_trace(1) - - # Ask for confirmation - await confirms.confirm_transaction(self.ctx, tsx_data, self.creds) - gc.collect() - self._mem_trace(3) - - # Basic transaction parameters - self.input_count = tsx_data.num_inputs - self.output_count = len(tsx_data.outputs) - self.output_change = misc.dst_entry_to_stdobj(tsx_data.change_dts) - self.mixin = tsx_data.mixin - self.fee = tsx_data.fee - self.account_idx = tsx_data.account - self.multi_sig = tsx_data.is_multisig - self.state.inp_cnt() - self.check_change(tsx_data.outputs) - self.exp_tx_prefix_hash = tsx_data.exp_tx_prefix_hash - - # Rsig data - self.rsig_type = tsx_data.rsig_data.rsig_type - self.rsig_grp = tsx_data.rsig_data.grouping - self.rsig_offload = self.rsig_type > 0 and self.output_count > 2 - self.use_bulletproof = self.rsig_type > 0 - self.use_simple_rct = self.input_count > 1 or self.rsig_type != 0 - - # Provided tx key, used mostly in multisig. - if len(tsx_data.use_tx_keys) > 0: - for ckey in tsx_data.use_tx_keys: - crypto.check_sc(crypto.decodeint(ckey)) - - self.tx_priv = crypto.decodeint(tsx_data.use_tx_keys[0]) - self.tx_pub = crypto.scalarmult_base(self.tx_priv) - self.additional_tx_private_keys = [ - crypto.decodeint(x) for x in tsx_data.use_tx_keys[1:] - ] - - # Additional keys w.r.t. subaddress destinations - class_res = classify_subaddresses(tsx_data.outputs, self.change_address()) - num_stdaddresses, num_subaddresses, single_dest_subaddress = class_res - - # if this is a single-destination transfer to a subaddress, we set the tx pubkey to R=r*D - if num_stdaddresses == 0 and num_subaddresses == 1: - self.tx_pub = crypto.scalarmult( - crypto.decodepoint(single_dest_subaddress.spend_public_key), - self.tx_priv, - ) - - self.need_additional_txkeys = num_subaddresses > 0 and ( - num_stdaddresses > 0 or num_subaddresses > 1 - ) - self._mem_trace(4, True) - - # Extra processing, payment id - self.tx.version = 2 - self.tx.unlock_time = tsx_data.unlock_time - self.process_payment_id(tsx_data) - await self.compute_sec_keys(tsx_data) - gc.collect() - - # Iterative tx_prefix_hash hash computation - self.tx_prefix_hasher.keep() - self.tx_prefix_hasher.uvarint(self.tx.version) - self.tx_prefix_hasher.uvarint(self.tx.unlock_time) - self.tx_prefix_hasher.container_size(self.num_inputs()) # ContainerType - self.tx_prefix_hasher.release() - self._mem_trace(10, True) - - # Final message hasher - self.full_message_hasher.init(self.use_simple_rct) - self.full_message_hasher.set_type_fee(self.get_rct_type(), self.get_fee()) - - # Sub address precomputation - if tsx_data.account is not None and tsx_data.minor_indices: - self.precompute_subaddr(tsx_data.account, tsx_data.minor_indices) - self._mem_trace(5, True) - - # HMAC outputs - pinning - hmacs = [] - for idx in range(self.num_dests()): - c_hmac = await keys.gen_hmac_tsxdest( - self.key_hmac, tsx_data.outputs[idx], idx - ) - hmacs.append(c_hmac) - gc.collect() - - self._mem_trace(6) - - from trezor.messages.MoneroTransactionInitAck import MoneroTransactionInitAck - from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData - - rsig_data = MoneroTransactionRsigData(offload_type=self.rsig_offload) - return MoneroTransactionInitAck( - in_memory=False, - many_inputs=True, - many_outputs=True, - hmacs=hmacs, - rsig_data=rsig_data, - ) - - def process_payment_id(self, tsx_data): - """ - Payment id -> extra - """ - if common.is_empty(tsx_data.payment_id): - return - - from apps.monero.xmr.sub import tsx_helper - - if len(tsx_data.payment_id) == 8: - view_key_pub_enc = tsx_helper.get_destination_view_key_pub( - tsx_data.outputs, self.change_address() - ) - if view_key_pub_enc == crypto.NULL_KEY_ENC: - raise ValueError( - "Destinations have to have exactly one output to support encrypted payment ids" - ) - - view_key_pub = crypto.decodepoint(view_key_pub_enc) - payment_id_encr = tsx_helper.encrypt_payment_id( - tsx_data.payment_id, view_key_pub, self.tx_priv - ) - - extra_nonce = payment_id_encr - extra_prefix = 1 - - elif len(tsx_data.payment_id) == 32: - extra_nonce = tsx_data.payment_id - extra_prefix = 0 - - else: - raise ValueError("Payment ID size invalid") - - lextra = len(extra_nonce) - if lextra >= 255: - raise ValueError("Nonce could be 255 bytes max") - - extra_buff = bytearray(3 + lextra) - extra_buff[0] = 2 - extra_buff[1] = lextra + 1 - extra_buff[2] = extra_prefix - utils.memcpy(extra_buff, 3, extra_nonce, 0, lextra) - self.tx.extra = extra_buff - - async def compute_sec_keys(self, tsx_data): - """ - Generate master key H(TsxData || r) - :return: - """ - import protobuf - from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer - - writer = get_keccak_writer() - await protobuf.dump_message(writer, tsx_data) - writer.write(crypto.encodeint(self.tx_priv)) - - self.key_master = crypto.keccak_2hash( - writer.get_digest() + crypto.encodeint(crypto.random_scalar()) - ) - self.key_hmac = crypto.keccak_2hash(b"hmac" + self.key_master) - self.key_enc = crypto.keccak_2hash(b"enc" + self.key_master) - - def precompute_subaddr(self, account, indices): - """ - Precomputes subaddresses for account (major) and list of indices (minors) - Subaddresses have to be stored in encoded form - unique representation. - Single point can have multiple extended coordinates representation - would not match during subaddress search. - """ - monero.compute_subaddresses(self.creds, account, indices, self.subaddresses) - - async def set_input(self, src_entr): - """ - Sets UTXO one by one. - Computes spending secret key, key image. tx.vin[i] + HMAC, Pedersen commitment on amount. - - If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are kept in the Trezor. - Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under Chacha20Poly1305() - with key derived for exactly this purpose. - """ - from trezor.messages.MoneroTransactionSetInputAck import ( - MoneroTransactionSetInputAck - ) - from apps.monero.xmr.enc import chacha_poly - from apps.monero.xmr.sub import tsx_helper - from apps.monero.xmr.serialize_messages.tx_prefix import TxinToKey - - self.state.input() - self.inp_idx += 1 - - await confirms.transaction_step( - self.ctx, self.STEP_INP, self.inp_idx, self.num_inputs() - ) - - if self.inp_idx >= self.num_inputs(): - raise ValueError("Too many inputs") - if src_entr.real_output >= len(src_entr.outputs): - raise ValueError( - "real_output index %s bigger than output_keys.size() %s" - % (src_entr.real_output, len(src_entr.outputs)) - ) - self.summary_inputs_money += src_entr.amount - - # Secrets derivation - out_key = crypto.decodepoint(src_entr.outputs[src_entr.real_output].key.dest) - tx_key = crypto.decodepoint(src_entr.real_out_tx_key) - additional_keys = [ - crypto.decodepoint(x) for x in src_entr.real_out_additional_tx_keys - ] - - secs = monero.generate_tx_spend_and_key_image_and_derivation( - self.creds, - self.subaddresses, - out_key, - tx_key, - additional_keys, - src_entr.real_output_in_tx_index, - ) - xi, ki, di = secs - self._mem_trace(1, True) - - # Construct tx.vin - ki_real = src_entr.multisig_kLRki.ki if self.multi_sig else ki - vini = TxinToKey(amount=src_entr.amount, k_image=crypto.encodepoint(ki_real)) - vini.key_offsets = tsx_helper.absolute_output_offsets_to_relative( - [x.idx for x in src_entr.outputs] - ) - - if src_entr.rct: - vini.amount = 0 - - # Serialize with variant code for TxinToKey - vini_bin = misc.dump_msg(vini, preallocate=64, prefix=b"\x02") - self._mem_trace(2, True) - - # HMAC(T_in,i || vin_i) - hmac_vini = await keys.gen_hmac_vini( - self.key_hmac, src_entr, vini_bin, self.inp_idx - ) - self._mem_trace(3, True) - - # PseudoOuts commitment, alphas stored to state - pseudo_out = None - pseudo_out_hmac = None - alpha_enc = None - - if self.use_simple_rct: - alpha, pseudo_out = self._gen_commitment(src_entr.amount) - pseudo_out = crypto.encodepoint(pseudo_out) - - # In full version the alpha is encrypted and passed back for storage - pseudo_out_hmac = crypto.compute_hmac( - keys.hmac_key_txin_comm(self.key_hmac, self.inp_idx), pseudo_out - ) - alpha_enc = chacha_poly.encrypt_pack( - keys.enc_key_txin_alpha(self.key_enc, self.inp_idx), - crypto.encodeint(alpha), - ) - - spend_enc = chacha_poly.encrypt_pack( - keys.enc_key_spend(self.key_enc, self.inp_idx), crypto.encodeint(xi) - ) - - # All inputs done? - if self.inp_idx + 1 == self.num_inputs(): - self.tsx_inputs_done() - - return MoneroTransactionSetInputAck( - vini=vini_bin, - vini_hmac=hmac_vini, - pseudo_out=pseudo_out, - pseudo_out_hmac=pseudo_out_hmac, - alpha_enc=alpha_enc, - spend_enc=spend_enc, - ) - - def tsx_inputs_done(self): - """ - All inputs set - """ - self.state.input_done() - self.subaddresses = None - - if self.inp_idx + 1 != self.num_inputs(): - raise ValueError("Input count mismatch") - - async def tsx_inputs_permutation(self, permutation): - """ - Set permutation on the inputs - sorted by key image on host. - """ - from trezor.messages.MoneroTransactionInputsPermutationAck import ( - MoneroTransactionInputsPermutationAck - ) - - await confirms.transaction_step(self.ctx, self.STEP_PERM) - - self._tsx_inputs_permutation(permutation) - return MoneroTransactionInputsPermutationAck() - - def _tsx_inputs_permutation(self, permutation): - """ - Set permutation on the inputs - sorted by key image on host. - """ - self.state.input_permutation() - self.source_permutation = permutation - common.check_permutation(permutation) - self.inp_idx = -1 - - async def input_vini(self, src_entr, vini_bin, hmac, pseudo_out, pseudo_out_hmac): - """ - Set tx.vin[i] for incremental tx prefix hash computation. - After sorting by key images on host. - Hashes pseudo_out to the final_message. - """ - from trezor.messages.MoneroTransactionInputViniAck import ( - MoneroTransactionInputViniAck - ) - - await confirms.transaction_step( - self.ctx, self.STEP_VINI, self.inp_idx + 1, self.num_inputs() - ) - - if self.inp_idx >= self.num_inputs(): - raise ValueError("Too many inputs") - - self.state.input_vins() - self.inp_idx += 1 - - # HMAC(T_in,i || vin_i) - hmac_vini = await keys.gen_hmac_vini( - self.key_hmac, src_entr, vini_bin, self.source_permutation[self.inp_idx] - ) - if not common.ct_equal(hmac_vini, hmac): - raise ValueError("HMAC is not correct") - - self.hash_vini_pseudo_out(vini_bin, self.inp_idx, pseudo_out, pseudo_out_hmac) - return MoneroTransactionInputViniAck() - - def hash_vini_pseudo_out( - self, vini_bin, inp_idx, pseudo_out=None, pseudo_out_hmac=None - ): - """ - Incremental hasing of tx.vin[i] and pseudo output - """ - self.tx_prefix_hasher.buffer(vini_bin) - - # Pseudo_out incremental hashing - applicable only in simple rct - if not self.use_simple_rct or self.use_bulletproof: - return - - idx = self.source_permutation[inp_idx] - pseudo_out_hmac_comp = crypto.compute_hmac( - keys.hmac_key_txin_comm(self.key_hmac, idx), pseudo_out - ) - if not common.ct_equal(pseudo_out_hmac, pseudo_out_hmac_comp): - raise ValueError("HMAC invalid for pseudo outs") - - self.full_message_hasher.set_pseudo_out(pseudo_out) - - async def all_in_set(self, rsig_data): - """ - If in the applicable offloading mode, generate commitment masks. - """ - self._mem_trace(0) - self.state.input_all_done() - await confirms.transaction_step(self.ctx, self.STEP_ALL_IN) - - from trezor.messages.MoneroTransactionAllInputsSetAck import ( - MoneroTransactionAllInputsSetAck - ) - from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData - - rsig_data = MoneroTransactionRsigData() - resp = MoneroTransactionAllInputsSetAck(rsig_data=rsig_data) - - if not self.rsig_offload: - return resp - - # Simple offloading - generate random masks that sum to the input mask sum. - tmp_buff = bytearray(32) - rsig_data.mask = bytearray(32 * self.num_dests()) - self.sumout = crypto.sc_init(0) - for i in range(self.num_dests()): - cur_mask = crypto.new_scalar() - is_last = i + 1 == self.num_dests() - if is_last and self.use_simple_rct: - crypto.sc_sub_into(cur_mask, self.sumpouts_alphas, self.sumout) - else: - crypto.random_scalar(cur_mask) - - crypto.sc_add_into(self.sumout, self.sumout, cur_mask) - self.output_masks.append(cur_mask) - crypto.encodeint_into(tmp_buff, cur_mask) - utils.memcpy(rsig_data.mask, 32 * i, tmp_buff, 0, 32) - - self.assrt(crypto.sc_eq(self.sumout, self.sumpouts_alphas), "Invalid masks sum") - self.sumout = crypto.sc_init(0) - return resp - - def _get_out_mask(self, idx): - if self.rsig_offload: - return self.output_masks[idx] - else: - is_last = idx + 1 == self.num_dests() - if is_last: - return crypto.sc_sub(self.sumpouts_alphas, self.sumout) - else: - return crypto.random_scalar() - - def _get_rsig_batch(self, idx): - """ - Returns index of the current rsig batch - """ - r = 0 - c = 0 - while c < idx + 1: - c += self.rsig_grp[r] - r += 1 - return r - 1 - - def _is_last_in_batch(self, idx, bidx=None): - """ - Returns true if the current output is last in the rsig batch - """ - bidx = self._get_rsig_batch(idx) if bidx is None else bidx - batch_size = self.rsig_grp[bidx] - return (idx - sum(self.rsig_grp[:bidx])) + 1 == batch_size - - def _gen_commitment(self, in_amount): - """ - Computes Pedersen commitment - pseudo outs - Here is slight deviation from the original protocol. - We want that \\sum Alpha = \\sum A_{i,j} where A_{i,j} is a mask from range proof for output i, bit j. - - Previously this was computed in such a way that Alpha_{last} = \\sum A{i,j} - \\sum_{i=0}^{last-1} Alpha - But we would prefer to compute commitment before range proofs so alphas are generated completely randomly - and the last A mask is computed in this special way. - Returns pseudo_out - """ - alpha = crypto.random_scalar() - self.sumpouts_alphas = crypto.sc_add(self.sumpouts_alphas, alpha) - return alpha, crypto.gen_commitment(alpha, in_amount) - - def _check_out_commitment(self, amount, mask, C): - self.assrt( - crypto.point_eq( - C, - crypto.point_add( - crypto.scalarmult_base(mask), crypto.scalarmult_h(amount) - ), - ), - "OutC fail", - ) - - def _return_rsig_data(self, rsig): - if rsig is None: - return None - from trezor.messages.MoneroTransactionRsigData import MoneroTransactionRsigData - - if isinstance(rsig, list): - return MoneroTransactionRsigData(rsig_parts=rsig) - else: - return MoneroTransactionRsigData(rsig=rsig) - - def _range_proof(self, idx, amount, rsig_data=None): - """ - Computes rangeproof and related information - out_sk, out_pk, ecdh_info. - In order to optimize incremental transaction build, the mask computation is changed compared - to the official Monero code. In the official code, the input pedersen commitments are computed - after range proof in such a way summed masks for commitments (alpha) and rangeproofs (ai) are equal. - - In order to save roundtrips we compute commitments randomly and then for the last rangeproof - a[63] = (\\sum_{i=0}^{num_inp}alpha_i - \\sum_{i=0}^{num_outs-1} amasks_i) - \\sum_{i=0}^{62}a_i - - The range proof is incrementally hashed to the final_message. - """ - from apps.monero.xmr import ring_ct - - mask = self._get_out_mask(idx) - self.output_amounts.append(amount) - provided_rsig = ( - rsig_data.rsig - if rsig_data and rsig_data.rsig and len(rsig_data.rsig) > 0 - else None - ) - if not self.rsig_offload and provided_rsig: - raise misc.TrezorError("Provided unexpected rsig") - if not self.rsig_offload: - self.output_masks.append(mask) - - # Batching - bidx = self._get_rsig_batch(idx) - batch_size = self.rsig_grp[bidx] - last_in_batch = self._is_last_in_batch(idx, bidx) - if self.rsig_offload and provided_rsig and not last_in_batch: - raise misc.TrezorError("Provided rsig too early") - if self.rsig_offload and last_in_batch and not provided_rsig: - raise misc.TrezorError("Rsig expected, not provided") - - # Batch not finished, skip range sig generation now - if not last_in_batch: - return None, mask - - # Rangeproof - # Pedersen commitment on the value, mask from the commitment, range signature. - C, rsig = None, None - - self._mem_trace("pre-rproof" if __debug__ else None, collect=True) - if not self.rsig_offload and self.use_bulletproof: - rsig = ring_ct.prove_range_bp_batch(self.output_amounts, self.output_masks) - self._mem_trace("post-bp" if __debug__ else None, collect=True) - - # Incremental hashing - self.full_message_hasher.rsig_val(rsig, True, raw=False) - self._mem_trace("post-bp-hash" if __debug__ else None, collect=True) - - rsig = misc.dump_rsig_bp(rsig) - self._mem_trace( - "post-bp-ser, size: %s" % len(rsig) if __debug__ else None, collect=True - ) - - elif not self.rsig_offload and not self.use_bulletproof: - C, mask, rsig = ring_ct.prove_range_chunked(amount, mask) - del (ring_ct) - - # Incremental hashing - self.full_message_hasher.rsig_val(rsig, False, raw=True) - self._check_out_commitment(amount, mask, C) - - elif self.rsig_offload and self.use_bulletproof: - from apps.monero.xmr.serialize_messages.tx_rsig_bulletproof import ( - Bulletproof - ) - - masks = [ - self._get_out_mask(1 + idx - batch_size + ix) - for ix in range(batch_size) - ] - - bp_obj = misc.parse_msg(rsig_data.rsig, Bulletproof()) - rsig_data.rsig = None - - self.full_message_hasher.rsig_val(bp_obj, True, raw=False) - res = ring_ct.verify_bp(bp_obj, self.output_amounts, masks) - self.assrt(res, "BP verification fail") - self._mem_trace("BP verified" if __debug__ else None, collect=True) - del (bp_obj, ring_ct) - - elif self.rsig_offload and not self.use_bulletproof: - self.full_message_hasher.rsig_val(rsig_data.rsig, False, raw=True) - rsig_data.rsig = None - - else: - raise misc.TrezorError("Unexpected rsig state") - - self._mem_trace("rproof" if __debug__ else None, collect=True) - self.output_amounts = [] - if not self.rsig_offload: - self.output_masks = [] - return rsig, mask - - def _set_out1_ecdh(self, dest_pub_key, amount, mask, amount_key): - from apps.monero.xmr import ring_ct - - # Mask sum - out_pk = misc.StdObj( - dest=crypto.encodepoint(dest_pub_key), - mask=crypto.encodepoint(crypto.gen_commitment(mask, amount)), - ) - self.sumout = crypto.sc_add(self.sumout, mask) - self.output_sk.append(misc.StdObj(mask=mask)) - - # ECDH masking - from apps.monero.xmr.sub.recode import recode_ecdh - - ecdh_info = misc.StdObj(mask=mask, amount=crypto.sc_init(amount)) - ring_ct.ecdh_encode_into( - ecdh_info, ecdh_info, derivation=crypto.encodeint(amount_key) - ) - recode_ecdh(ecdh_info, encode=True) - - ecdh_info_bin = bytearray(64) - utils.memcpy(ecdh_info_bin, 0, ecdh_info.mask, 0, 32) - utils.memcpy(ecdh_info_bin, 32, ecdh_info.amount, 0, 32) - gc.collect() - - return out_pk, ecdh_info_bin - - def _set_out1_additional_keys(self, dst_entr): - additional_txkey = None - additional_txkey_priv = None - if self.need_additional_txkeys: - use_provided = self.num_dests() == len(self.additional_tx_private_keys) - additional_txkey_priv = ( - self.additional_tx_private_keys[self.out_idx] - if use_provided - else crypto.random_scalar() - ) - - if dst_entr.is_subaddress: - additional_txkey = crypto.scalarmult( - crypto.decodepoint(dst_entr.addr.spend_public_key), - additional_txkey_priv, - ) - else: - additional_txkey = crypto.scalarmult_base(additional_txkey_priv) - - self.additional_tx_public_keys.append(crypto.encodepoint(additional_txkey)) - if not use_provided: - self.additional_tx_private_keys.append(additional_txkey_priv) - return additional_txkey_priv - - def _set_out1_derivation(self, dst_entr, additional_txkey_priv): - from apps.monero.xmr.sub.addr import addr_eq - - change_addr = self.change_address() - if change_addr and addr_eq(dst_entr.addr, change_addr): - # sending change to yourself; derivation = a*R - derivation = crypto.generate_key_derivation( - self.tx_pub, self.creds.view_key_private - ) - - else: - # sending to the recipient; derivation = r*A (or s*C in the subaddress scheme) - deriv_priv = ( - additional_txkey_priv - if dst_entr.is_subaddress and self.need_additional_txkeys - else self.tx_priv - ) - derivation = crypto.generate_key_derivation( - crypto.decodepoint(dst_entr.addr.view_public_key), deriv_priv - ) - return derivation - - async def _set_out1_tx_out(self, dst_entr, tx_out_key): - # Manual serialization of TxOut(0, TxoutToKey(key)) - tx_out_bin = bytearray(34) - tx_out_bin[0] = 0 # amount varint - tx_out_bin[1] = 2 # variant code TxoutToKey - crypto.encodepoint_into(tx_out_bin, tx_out_key, 2) - self._mem_trace(8) - - # Tx header prefix hashing - self.tx_prefix_hasher.buffer(tx_out_bin) - self._mem_trace(9, True) - - # Hmac dest_entr. - hmac_vouti = await keys.gen_hmac_vouti( - self.key_hmac, dst_entr, tx_out_bin, self.out_idx - ) - self._mem_trace(10, True) - return tx_out_bin, hmac_vouti - - async def set_out1(self, dst_entr, dst_entr_hmac, rsig_data=None): - """ - Set destination entry one by one. - Computes destination stealth address, amount key, range proof + HMAC, out_pk, ecdh_info. - """ - self._mem_trace(0, True) - mods = utils.unimport_begin() - - await confirms.transaction_step( - self.ctx, self.STEP_OUT, self.out_idx + 1, self.num_dests() - ) - self._mem_trace(1) - - if self.state.is_input_vins() and self.inp_idx + 1 != self.num_inputs(): - raise ValueError("Invalid number of inputs") - - self.state.set_output() - self.out_idx += 1 - self._mem_trace(2, True) - - if dst_entr.amount <= 0 and self.tx.version <= 1: - raise ValueError("Destination with wrong amount: %s" % dst_entr.amount) - - # HMAC check of the destination - dst_entr_hmac_computed = await keys.gen_hmac_tsxdest( - self.key_hmac, dst_entr, self.out_idx - ) - if not common.ct_equal(dst_entr_hmac, dst_entr_hmac_computed): - raise ValueError("HMAC invalid") - del (dst_entr_hmac, dst_entr_hmac_computed) - self._mem_trace(3, True) - - # First output - tx prefix hasher - size of the container - if self.out_idx == 0: - self.tx_prefix_hasher.container_size(self.num_dests()) - self._mem_trace(4, True) - - self.summary_outs_money += dst_entr.amount - utils.unimport_end(mods) - self._mem_trace(5, True) - - # Range proof first, memory intensive - rsig, mask = self._range_proof(self.out_idx, dst_entr.amount, rsig_data) - utils.unimport_end(mods) - self._mem_trace(6, True) - - # Amount key, tx out key - additional_txkey_priv = self._set_out1_additional_keys(dst_entr) - derivation = self._set_out1_derivation(dst_entr, additional_txkey_priv) - amount_key = crypto.derivation_to_scalar(derivation, self.out_idx) - tx_out_key = crypto.derive_public_key( - derivation, self.out_idx, crypto.decodepoint(dst_entr.addr.spend_public_key) - ) - del (derivation, additional_txkey_priv) - self._mem_trace(7, True) - - # Tx header prefix hashing, hmac dst_entr - tx_out_bin, hmac_vouti = await self._set_out1_tx_out(dst_entr, tx_out_key) - self._mem_trace(11, True) - - # Out_pk, ecdh_info - out_pk, ecdh_info_bin = self._set_out1_ecdh( - dest_pub_key=tx_out_key, - amount=dst_entr.amount, - mask=mask, - amount_key=amount_key, - ) - del (dst_entr, mask, amount_key, tx_out_key) - self._mem_trace(12, True) - - # Incremental hashing of the ECDH info. - # RctSigBase allows to hash only one of the (ecdh, out_pk) as they are serialized - # as whole vectors. Hashing ECDH info saves state space. - self.full_message_hasher.set_ecdh(ecdh_info_bin) - self._mem_trace(13, True) - - # Output_pk is stored to the state as it is used during the signature and hashed to the - # RctSigBase later. - self.output_pk.append(out_pk) - self._mem_trace(14, True) - - from trezor.messages.MoneroTransactionSetOutputAck import ( - MoneroTransactionSetOutputAck - ) - - out_pk_bin = bytearray(64) - utils.memcpy(out_pk_bin, 0, out_pk.dest, 0, 32) - utils.memcpy(out_pk_bin, 32, out_pk.mask, 0, 32) - - return MoneroTransactionSetOutputAck( - tx_out=tx_out_bin, - vouti_hmac=hmac_vouti, - rsig_data=self._return_rsig_data(rsig), - out_pk=out_pk_bin, - ecdh_info=ecdh_info_bin, - ) - - def all_out1_set_tx_extra(self): - from apps.monero.xmr.sub import tsx_helper - - self.tx.extra = tsx_helper.add_tx_pub_key_to_extra(self.tx.extra, self.tx_pub) - - # Not needed to remove - extra is clean - # self.tx.extra = await monero.remove_field_from_tx_extra(self.tx.extra, xmrtypes.TxExtraAdditionalPubKeys) - if self.need_additional_txkeys: - self.tx.extra = tsx_helper.add_additional_tx_pub_keys_to_extra( - self.tx.extra, pub_enc=self.additional_tx_public_keys - ) - - def all_out1_set_tx_prefix(self): - from apps.monero.xmr.serialize.message_types import BlobType - - self.tx_prefix_hasher.message_field(self.tx, ("extra", BlobType)) # extra - - self.tx_prefix_hash = self.tx_prefix_hasher.get_digest() - self.tx_prefix_hasher = None - - # Hash message to the final_message - self.full_message_hasher.set_message(self.tx_prefix_hash) - - async def all_out1_set(self): - """ - All outputs were set in this phase. Computes additional public keys (if needed), tx.extra and - transaction prefix hash. - Adds additional public keys to the tx.extra - - :return: tx.extra, tx_prefix_hash - """ - self._mem_trace(0) - self.state.set_output_done() - await confirms.transaction_step(self.ctx, self.STEP_ALL_OUT) - self._mem_trace(1) - - if self.out_idx + 1 != self.num_dests(): - raise ValueError("Invalid out num") - - # Test if \sum Alpha == \sum A - if self.use_simple_rct: - self.assrt(crypto.sc_eq(self.sumout, self.sumpouts_alphas)) - - # Fee test - if self.fee != (self.summary_inputs_money - self.summary_outs_money): - raise ValueError( - "Fee invalid %s vs %s, out: %s" - % ( - self.fee, - self.summary_inputs_money - self.summary_outs_money, - self.summary_outs_money, - ) - ) - self._mem_trace(2) - - # Set public key to the extra - # Not needed to remove - extra is clean - self.all_out1_set_tx_extra() - self.additional_tx_public_keys = None - - gc.collect() - self._mem_trace(3) - - if self.summary_outs_money > self.summary_inputs_money: - raise ValueError( - "Transaction inputs money (%s) less than outputs money (%s)" - % (self.summary_inputs_money, self.summary_outs_money) - ) - - # Hashing transaction prefix - self.all_out1_set_tx_prefix() - extra_b = self.tx.extra - self.tx = None - gc.collect() - self._mem_trace(4) - - # Txprefix match check for multisig - if not common.is_empty(self.exp_tx_prefix_hash) and not common.ct_equal( - self.exp_tx_prefix_hash, self.tx_prefix_hash - ): - self.state.set_fail() - raise misc.TrezorTxPrefixHashNotMatchingError("Tx prefix invalid") - - gc.collect() - self._mem_trace(5) - - from trezor.messages.MoneroRingCtSig import MoneroRingCtSig - from trezor.messages.MoneroTransactionAllOutSetAck import ( - MoneroTransactionAllOutSetAck - ) - - rv = self.init_rct_sig() - rv_pb = MoneroRingCtSig(txn_fee=rv.txnFee, message=rv.message, rv_type=rv.type) - return MoneroTransactionAllOutSetAck( - extra=extra_b, tx_prefix_hash=self.tx_prefix_hash, rv=rv_pb - ) - - def tsx_mlsag_ecdh_info(self): - """ - Sets ecdh info for the incremental hashing mlsag. - """ - pass - - def tsx_mlsag_out_pk(self): - """ - Sets out_pk for the incremental hashing mlsag. - """ - if self.num_dests() != len(self.output_pk): - raise ValueError("Invalid number of ecdh") - - for out in self.output_pk: - self.full_message_hasher.set_out_pk(out) - - async def mlsag_done(self): - """ - MLSAG message computed. - """ - from trezor.messages.MoneroTransactionMlsagDoneAck import ( - MoneroTransactionMlsagDoneAck - ) - - self.state.set_final_message_done() - await confirms.transaction_step(self.ctx, self.STEP_MLSAG) - - self.tsx_mlsag_ecdh_info() - self.tsx_mlsag_out_pk() - self.full_message_hasher.rctsig_base_done() - self.out_idx = -1 - self.inp_idx = -1 - - self.full_message = self.full_message_hasher.get_digest() - self.full_message_hasher = None - - return MoneroTransactionMlsagDoneAck(full_message_hash=self.full_message) - - async def sign_input( - self, - src_entr, - vini_bin, - hmac_vini, - pseudo_out, - pseudo_out_hmac, - alpha_enc, - spend_enc, - ): - """ - Generates a signature for one input. - - :param src_entr: Source entry - :param vini_bin: tx.vin[i] for the transaction. Contains key image, offsets, amount (usually zero) - :param hmac_vini: HMAC for the tx.vin[i] as returned from Trezor - :param pseudo_out: pedersen commitment for the current input, uses alpha as the mask. - Only in memory offloaded scenario. Tuple containing HMAC, as returned from the Trezor. - :param pseudo_out_hmac: - :param alpha_enc: alpha mask for the current input. Only in memory offloaded scenario, - tuple as returned from the Trezor - :param spend_enc: - :return: Generated signature MGs[i] - """ - self.state.set_signature() - await confirms.transaction_step( - self.ctx, self.STEP_SIGN, self.inp_idx + 1, self.num_inputs() - ) - - self.inp_idx += 1 - if self.inp_idx >= self.num_inputs(): - raise ValueError("Invalid ins") - if self.use_simple_rct and alpha_enc is None: - raise ValueError("Inconsistent1") - if self.use_simple_rct and pseudo_out is None: - raise ValueError("Inconsistent2") - if self.inp_idx >= 1 and not self.use_simple_rct: - raise ValueError("Inconsistent3") - - inv_idx = self.source_permutation[self.inp_idx] - - # Check HMAC of all inputs - hmac_vini_comp = await keys.gen_hmac_vini( - self.key_hmac, src_entr, vini_bin, inv_idx - ) - if not common.ct_equal(hmac_vini_comp, hmac_vini): - raise ValueError("HMAC is not correct") - - gc.collect() - self._mem_trace(1) - - if self.use_simple_rct: - pseudo_out_hmac_comp = crypto.compute_hmac( - keys.hmac_key_txin_comm(self.key_hmac, inv_idx), pseudo_out - ) - if not common.ct_equal(pseudo_out_hmac_comp, pseudo_out_hmac): - raise ValueError("HMAC is not correct") - - gc.collect() - self._mem_trace(2) - - from apps.monero.xmr.enc import chacha_poly - - alpha_c = crypto.decodeint( - chacha_poly.decrypt_pack( - keys.enc_key_txin_alpha(self.key_enc, inv_idx), bytes(alpha_enc) - ) - ) - pseudo_out_c = crypto.decodepoint(pseudo_out) - - elif self.use_simple_rct: - alpha_c = self.input_alphas[self.inp_idx] - pseudo_out_c = crypto.decodepoint(self.input_pseudo_outs[self.inp_idx]) - - else: - alpha_c = None - pseudo_out_c = None - - # Spending secret - from apps.monero.xmr.enc import chacha_poly - - input_secret = crypto.decodeint( - chacha_poly.decrypt_pack( - keys.enc_key_spend(self.key_enc, inv_idx), bytes(spend_enc) - ) - ) - - gc.collect() - self._mem_trace(3) - - # Basic setup, sanity check - index = src_entr.real_output - in_sk = misc.StdObj(dest=input_secret, mask=crypto.decodeint(src_entr.mask)) - kLRki = src_entr.multisig_kLRki if self.multi_sig else None - - # Private key correctness test - self.assrt( - crypto.point_eq( - crypto.decodepoint(src_entr.outputs[src_entr.real_output].key.dest), - crypto.scalarmult_base(in_sk.dest), - ), - "a1", - ) - self.assrt( - crypto.point_eq( - crypto.decodepoint(src_entr.outputs[src_entr.real_output].key.mask), - crypto.gen_commitment(in_sk.mask, src_entr.amount), - ), - "a2", - ) - - gc.collect() - self._mem_trace(4) - - # RCT signature - gc.collect() - from apps.monero.xmr import mlsag2 - - if self.use_simple_rct: - # Simple RingCT - mix_ring = [x.key for x in src_entr.outputs] - mg, msc = mlsag2.prove_rct_mg_simple( - self.full_message, - mix_ring, - in_sk, - alpha_c, - pseudo_out_c, - kLRki, - None, - index, - ) - - else: - # Full RingCt, only one input - txn_fee_key = crypto.scalarmult_h(self.get_fee()) - mix_ring = [[x.key] for x in src_entr.outputs] - - mg, msc = mlsag2.prove_rct_mg( - self.full_message, - mix_ring, - [in_sk], - self.output_sk, - self.output_pk, - kLRki, - None, - index, - txn_fee_key, - ) - - gc.collect() - self._mem_trace(5) - - # Encode - from apps.monero.xmr.sub.recode import recode_msg - - mgs = recode_msg([mg]) - cout = None - - gc.collect() - self._mem_trace(6) - - # Multisig values returned encrypted, keys returned after finished successfully. - if self.multi_sig: - from apps.monero.xmr.enc import chacha_poly - - cout = chacha_poly.encrypt_pack( - keys.enc_key_cout(self.key_enc), crypto.encodeint(msc) - ) - - # Final state transition - if self.inp_idx + 1 == self.num_inputs(): - self.state.set_signature_done() - await confirms.transaction_signed(self.ctx) - - gc.collect() - self._mem_trace() - - from trezor.messages.MoneroTransactionSignInputAck import ( - MoneroTransactionSignInputAck - ) - - return MoneroTransactionSignInputAck( - signature=misc.dump_msg_gc(mgs[0], preallocate=488, del_msg=True), cout=cout - ) - - async def final_msg(self): - """ - Final step after transaction signing. - """ - from trezor.messages.MoneroTransactionFinalAck import MoneroTransactionFinalAck - from apps.monero.xmr.enc import chacha_poly - - self.state.set_final() - - cout_key = keys.enc_key_cout(self.key_enc) if self.multi_sig else None - - # Encrypted tx keys under transaction specific key, derived from txhash and spend key. - # Deterministic transaction key, so we can recover it just from transaction and the spend key. - tx_key, salt, rand_mult = misc.compute_tx_key( - self.creds.spend_key_private, self.tx_prefix_hash - ) - - key_buff = crypto.encodeint(self.tx_priv) + b"".join( - [crypto.encodeint(x) for x in self.additional_tx_private_keys] - ) - tx_enc_keys = chacha_poly.encrypt_pack(tx_key, key_buff) - - await confirms.transaction_finished(self.ctx) - gc.collect() - - return MoneroTransactionFinalAck( - cout_key=cout_key, salt=salt, rand_mult=rand_mult, tx_enc_keys=tx_enc_keys - ) diff --git a/src/apps/monero/protocol/tsx_sign_state.py b/src/apps/monero/protocol/tsx_sign_state.py deleted file mode 100644 index d2f805203..000000000 --- a/src/apps/monero/protocol/tsx_sign_state.py +++ /dev/null @@ -1,101 +0,0 @@ -from micropython import const - - -class TState: - """ - Transaction state - """ - - START = const(0) - INIT = const(1) - INP_CNT = const(2) - INPUT = const(3) - INPUT_DONE = const(4) - INPUT_PERM = const(5) - INPUT_VINS = const(6) - INPUT_ALL_DONE = const(7) - OUTPUT = const(8) - RSIG = const(9) - OUTPUT_DONE = const(10) - FINAL_MESSAGE = const(11) - SIGNATURE = const(12) - SIGNATURE_DONE = const(13) - FINAL = const(14) - FAIL = const(250) - - def __init__(self): - self.s = self.START - - def init_tsx(self): - if self.s != self.START: - raise ValueError("Illegal state") - self.s = self.INIT - - def inp_cnt(self): - if self.s != self.INIT: - raise ValueError("Illegal state") - self.s = self.INP_CNT - - def input(self): - if self.s != self.INP_CNT and self.s != self.INPUT: - raise ValueError("Illegal state") - self.s = self.INPUT - - def input_done(self): - if self.s != self.INPUT: - raise ValueError("Illegal state") - self.s = self.INPUT_DONE - - def input_permutation(self): - if self.s != self.INPUT_DONE: - raise ValueError("Illegal state") - self.s = self.INPUT_PERM - - def input_vins(self): - if self.s != self.INPUT_PERM and self.s != self.INPUT_VINS: - raise ValueError("Illegal state") - self.s = self.INPUT_VINS - - def is_input_vins(self): - return self.s == self.INPUT_VINS - - def input_all_done(self): - if self.s != self.INPUT_VINS: - raise ValueError("Illegal state") - self.s = self.INPUT_ALL_DONE - - def set_output(self): - if self.s != self.INPUT_ALL_DONE and self.s != self.OUTPUT: - raise ValueError("Illegal state") - self.s = self.OUTPUT - - def set_output_done(self): - if self.s != self.OUTPUT: - raise ValueError("Illegal state") - self.s = self.OUTPUT_DONE - - def set_final_message_done(self): - if self.s != self.OUTPUT_DONE: - raise ValueError("Illegal state") - self.s = self.FINAL_MESSAGE - - def set_signature(self): - if self.s != self.FINAL_MESSAGE and self.s != self.SIGNATURE: - raise ValueError("Illegal state") - self.s = self.SIGNATURE - - def set_signature_done(self): - if self.s != self.SIGNATURE: - raise ValueError("Illegal state") - self.s = self.SIGNATURE_DONE - - def set_final(self): - if self.s != self.SIGNATURE_DONE: - raise ValueError("Illegal state") - self.s = self.FINAL - - def set_fail(self): - self.s = self.FAIL - - def is_terminal(self): - return self.s in [self.FINAL, self.FAIL] diff --git a/src/apps/monero/sign_tx.py b/src/apps/monero/sign_tx.py index 0720747b1..11942f2b4 100644 --- a/src/apps/monero/sign_tx.py +++ b/src/apps/monero/sign_tx.py @@ -1,223 +1,7 @@ -import gc - -from trezor import log, utils -from trezor.messages import MessageType +from apps.monero.protocol.signing import step_01_init_transaction async def sign_tx(ctx, msg): - state = None - gc.collect() - mods = utils.unimport_begin() - - while True: - if __debug__: - log.debug(__name__, "#### F: %s, A: %s", gc.mem_free(), gc.mem_alloc()) - res, state, accept_msgs = await sign_tx_step(ctx, msg, state) - if accept_msgs is None: - break - - await ctx.write(res) - del (res, msg) - utils.unimport_end(mods) - - msg = await ctx.read(accept_msgs) - gc.collect() - - utils.unimport_end(mods) - return res - - -async def sign_tx_step(ctx, msg, state): - gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) - gc.collect() - - from apps.monero.controller import misc - from apps.monero.protocol.tsx_sign_builder import TTransactionBuilder - - if msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionInitRequest: - creds = await misc.monero_get_creds(ctx, msg.address_n, msg.network_type) - state = TTransactionBuilder(ctx, creds) - del creds - - gc.collect() - res, accept_msgs = await sign_tx_dispatch(state, msg) - gc.collect() - - if state.is_terminal(): - state = None - return res, state, accept_msgs - - -async def sign_tx_dispatch(tsx, msg): - if msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionInitRequest: - return ( - await tsx_init(tsx, msg.tsx_data), - (MessageType.MoneroTransactionSetInputRequest,), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionSetInputRequest: - return ( - await tsx_set_input(tsx, msg), - ( - MessageType.MoneroTransactionSetInputRequest, - MessageType.MoneroTransactionInputsPermutationRequest, - ), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionInputsPermutationRequest: - return ( - await tsx_inputs_permutation(tsx, msg), - (MessageType.MoneroTransactionInputViniRequest,), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionInputViniRequest: - return ( - await tsx_input_vini(tsx, msg), - ( - MessageType.MoneroTransactionInputViniRequest, - MessageType.MoneroTransactionAllInputsSetRequest, - ), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionAllInputsSetRequest: - return ( - await tsx_all_in_set(tsx, msg), - (MessageType.MoneroTransactionSetOutputRequest,), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionSetOutputRequest: - return ( - await tsx_set_output1(tsx, msg), - ( - MessageType.MoneroTransactionSetOutputRequest, - MessageType.MoneroTransactionAllOutSetRequest, - ), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionAllOutSetRequest: - return ( - await tsx_all_out1_set(tsx, msg), - (MessageType.MoneroTransactionMlsagDoneRequest,), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionMlsagDoneRequest: - return ( - await tsx_mlsag_done(tsx), - (MessageType.MoneroTransactionSignInputRequest,), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionSignInputRequest: - return ( - await tsx_sign_input(tsx, msg), - ( - MessageType.MoneroTransactionSignInputRequest, - MessageType.MoneroTransactionFinalRequest, - ), - ) - - elif msg.MESSAGE_WIRE_TYPE == MessageType.MoneroTransactionFinalRequest: - return await tsx_sign_final(tsx), None - - else: - from trezor import wire - - raise wire.DataError("Unknown message") - - -async def tsx_init(tsx, tsx_data): - return await tsx.init_transaction(tsx_data) - - -async def tsx_set_input(tsx, msg): - """ - Sets UTXO one by one. - Computes spending secret key, key image. tx.vin[i] + HMAC, Pedersen commitment on amount. - - If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are kept in the Trezor. - Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under AES-GCM() with - key derived for exactly this purpose. - """ - return await tsx.set_input(msg.src_entr) - - -async def tsx_inputs_permutation(tsx, msg): - """ - Set permutation on the inputs - sorted by key image on host. - """ - return await tsx.tsx_inputs_permutation(msg.perm) - - -async def tsx_input_vini(tsx, msg): - """ - Set tx.vin[i] for incremental tx prefix hash computation. - After sorting by key images on host. - """ - return await tsx.input_vini( - msg.src_entr, msg.vini, msg.vini_hmac, msg.pseudo_out, msg.pseudo_out_hmac + return await step_01_init_transaction.init_transaction( + ctx, msg.address_n, msg.network_type, msg.tsx_data ) - - -async def tsx_all_in_set(tsx, msg): - """ - All inputs set. Defining rsig parameters. - """ - return await tsx.all_in_set(msg.rsig_data) - - -async def tsx_set_output1(tsx, msg): - """ - Set destination entry one by one. - Computes destination stealth address, amount key, range proof + HMAC, out_pk, ecdh_info. - """ - dst, dst_hmac, rsig_data = msg.dst_entr, msg.dst_entr_hmac, msg.rsig_data - del (msg) - - return await tsx.set_out1(dst, dst_hmac, rsig_data) - - -async def tsx_all_out1_set(tsx, msg): - """ - All outputs were set in this phase. Computes additional public keys (if needed), tx.extra and - transaction prefix hash. - Adds additional public keys to the tx.extra - - :return: tx.extra, tx_prefix_hash - """ - from apps.monero.controller.misc import TrezorTxPrefixHashNotMatchingError - - try: - return await tsx.all_out1_set() - except TrezorTxPrefixHashNotMatchingError as e: - from trezor import wire - - raise wire.NotEnoughFunds(e.message) - - -async def tsx_mlsag_done(tsx): - """ - MLSAG message computed. - """ - return await tsx.mlsag_done() - - -async def tsx_sign_input(tsx, msg): - """ - Generates a signature for one input. - """ - return await tsx.sign_input( - msg.src_entr, - msg.vini, - msg.vini_hmac, - msg.pseudo_out, - msg.pseudo_out_hmac, - msg.alpha_enc, - msg.spend_enc, - ) - - -async def tsx_sign_final(tsx): - """ - Final message. - Offloading tx related data, encrypted. - """ - return await tsx.final_msg()