def _check_rsig_data(state: State, rsig_data: MoneroTransactionRsigData):
    """
    There are two types of monero ring confidential transactions:
    1. RCTTypeFull = 1 (used if num_inputs == 1 && Borromean)
    2. RCTTypeSimple = 2 (for num_inputs > 1 || !Borromean)

    and four types of range proofs (set in `rsig_data.rsig_type`):
    1. RangeProofBorromean = 0
    2. RangeProofBulletproof = 1
    3. RangeProofMultiOutputBulletproof = 2
    4. RangeProofPaddedBulletproof = 3

    The current code supports only HF9, HF10 thus TX type is always simple
    and RCT algorithm is always Bulletproof.
    """
    state.rsig_grouping = rsig_data.grouping

    if rsig_data.rsig_type == 0:
        raise ValueError("Borromean range sig not supported")

    elif rsig_data.rsig_type not in (1, 2, 3):
        raise ValueError("Unknown rsig type")

    if state.output_count > 2:
        state.rsig_offload = True

    _check_grouping(state)
def _process_payment_id(state: State, tsx_data: MoneroTransactionData):
    """
    Writes payment id to the `extra` field under the TX_EXTRA_NONCE = 0x02 tag.

    The second tag describes if the payment id is encrypted or not.
    If the payment id is 8 bytes long it implies encryption and
    therefore the TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID = 0x01 tag is used.
    If it is not encrypted, we use TX_EXTRA_NONCE_PAYMENT_ID = 0x00.

    Since Monero release 0.13 all 2 output payments have encrypted payment ID
    to make BC more uniform.

    See:
    - https://github.com/monero-project/monero/blob/ff7dc087ae5f7de162131cea9dbcf8eac7c126a1/src/cryptonote_basic/tx_extra.h
    """
    # encrypted payment id / dummy payment ID
    view_key_pub_enc = None

    if not tsx_data.payment_id or len(tsx_data.payment_id) == 8:
        view_key_pub_enc = _get_key_for_payment_id_encryption(
            tsx_data, state.change_address(), state.client_version > 0)

    if not tsx_data.payment_id:
        return

    elif len(tsx_data.payment_id) == 8:
        view_key_pub = crypto.decodepoint(view_key_pub_enc)
        payment_id_encr = _encrypt_payment_id(tsx_data.payment_id,
                                              view_key_pub, state.tx_priv)

        extra_nonce = payment_id_encr
        extra_prefix = 1  # TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID

    # plain text payment id
    elif len(tsx_data.payment_id) == 32:
        extra_nonce = tsx_data.payment_id
        extra_prefix = 0  # TX_EXTRA_NONCE_PAYMENT_ID

    else:
        raise ValueError("Payment ID size invalid")

    lextra = len(extra_nonce)
    if lextra >= 255:
        raise ValueError("Nonce could be 255 bytes max")

    # write it to extra
    extra_buff = bytearray(3 + lextra)
    extra_buff[0] = 2  # TX_EXTRA_NONCE
    extra_buff[1] = lextra + 1
    extra_buff[2] = extra_prefix
    extra_buff[3:] = extra_nonce
    state.extra_nonce = extra_buff
async def _compute_sec_keys(state: State, tsx_data: MoneroTransactionData):
    """
    Generate master key H( H(TsxData || tx_priv) || rand )
    """
    import protobuf
    from apps.monero.xmr.keccak_hasher import get_keccak_writer

    writer = get_keccak_writer()
    await protobuf.dump_message(writer, tsx_data)
    writer.write(crypto.encodeint(state.tx_priv))

    master_key = crypto.keccak_2hash(writer.get_digest() +
                                     crypto.encodeint(crypto.random_scalar()))
    state.key_hmac = crypto.keccak_2hash(b"hmac" + master_key)
    state.key_enc = crypto.keccak_2hash(b"enc" + master_key)
def _check_change(state: State,
                  outputs: List[MoneroTransactionDestinationEntry]):
    """
    Check if the change address in state.output_change (from `tsx_data.outputs`) is
    a) among tx outputs
    b) is equal to our address

    The change output is in `tsx_data.change_dts`, but also has to be in `tsx_data.outputs`.
    This is what Monero does in its cold wallet signing protocol.

    In other words, these structures are built by Monero when generating unsigned transaction set
    and we do not want to modify this logic. We just translate the unsigned tx to the protobuf message.

    So, although we could probably optimize this by having the change output in `change_dts`
    only, we intentionally do not do so.
    """
    from apps.monero.xmr.addresses import addr_eq, get_change_addr_idx

    change_index = get_change_addr_idx(outputs, state.output_change)

    change_addr = state.change_address()
    # if there is no change, there is nothing to check
    if change_addr is None:
        state.mem_trace("No change" if __debug__ else None)
        return
    """
    Sweep tx is just one output and no change.
    To prevent recognition of such transactions another fake output is added
    that spends exactly 0 coins to a random address.
    See https://github.com/monero-project/monero/pull/1415
    """
    if change_index is None and state.output_change.amount == 0 and len(
            outputs) == 2:
        state.mem_trace("Sweep tsx" if __debug__ else None)
        return

    found = False
    for out in outputs:
        if addr_eq(out.addr, change_addr):
            found = True
            break

    if not found:
        raise signing.ChangeAddressError("Change address not found in outputs")

    my_addr = _get_primary_change_address(state)
    if not addr_eq(my_addr, change_addr):
        raise signing.ChangeAddressError("Change address differs from ours")
Esempio n. 5
0
async def sign_tx(ctx, received_msg, keychain):
    state = State(ctx)
    mods = utils.unimport_begin()

    # Splitting ctx.call() to write() and read() helps to reduce memory fragmentation
    # between calls.
    while True:
        if __debug__:
            log.debug(__name__, "#### F: %s, A: %s", gc.mem_free(),
                      gc.mem_alloc())
        gc.collect()
        gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())

        result_msg, accept_msgs = await sign_tx_dispatch(
            state, received_msg, keychain)
        if accept_msgs is None:
            break

        await ctx.write(result_msg)
        del (result_msg, received_msg)
        utils.unimport_end(mods)

        received_msg = await ctx.read(accept_msgs)

    utils.unimport_end(mods)
    return result_msg
Esempio n. 6
0
def _check_subaddresses(
        state: State,
        outputs: list[MoneroTransactionDestinationEntry]) -> None:
    """
    Using subaddresses leads to a few poorly documented exceptions.

    Normally we set R=r*G (tx_pub), however for subaddresses this is equal to R=r*D
    to achieve the nice blockchain scanning property.

    Remember, that R is per-transaction and not per-input. It's all good if we have a
    single output or we have a single destination and the second output is our change.
    This is because although the R=r*D, we can still derive the change using our private view-key.
    In other words, calculate the one-time address as P = H(x*R)*G + Y (where X,Y is the change).

    However, this does not work for other outputs than change, because we do not have the
    recipient's view key, so we cannot use the same formula -- we need a new R.

    The solution is very straightforward -- we create additional `R`s and use the `extra`
    field to include them under the `ADDITIONAL_PUBKEYS` tag.

    See:
    - https://lab.getmonero.org/pubs/MRL-0006.pdf
    - https://github.com/monero-project/monero/pull/2056
    """
    from apps.monero.xmr.addresses import classify_subaddresses

    # let's first figure out what kind of destinations we have
    num_stdaddresses, num_subaddresses, single_dest_subaddress = classify_subaddresses(
        outputs, state.change_address())

    # if this is a single-destination transfer to a subaddress,
    # we set (override) the tx pubkey to R=r*D and no additional
    # tx keys are needed
    if num_stdaddresses == 0 and num_subaddresses == 1:
        state.tx_pub = crypto.scalarmult_into(
            None,
            crypto_helpers.decodepoint(
                single_dest_subaddress.spend_public_key),
            state.tx_priv,
        )

    # if a subaddress is used and either standard address is as well
    # or more than one subaddress is used we need to add additional tx keys
    state.need_additional_txkeys = num_subaddresses > 0 and (
        num_stdaddresses > 0 or num_subaddresses > 1)
    state.mem_trace(4, True)
    def test_sig_seal(self):
        mst = ubinascii.unhexlify(
            b"ca3bbe08a178a4508c3992a47ba775799e7626a365ed136e803fe5f2df2ce01c"
        )
        st = State(None)
        st.last_step = st.STEP_SIGN
        st.opening_key = mst
        st.current_input_index = 3

        mg_buff = [
            '0b',
            '02fe9ee789007254215b41351109f186620624a3c1ad2ba89628194528672adf04f900ebf9ad3b0cc1ac9ae1f03167f74d6e04175df5001c91d09d29dbefd6bc0b',
            '021d46f6db8a349caca48a4dfee155b9dee927d0f25cdf5bcd724358c611b47906de6cedad47fd26070927f3954bcaf7a0e126699bf961ca4e8124abefe8aaeb05',
            '02ae933994effe2b348b09bfab783bf9adb58b09659d8f5bd058cca252d763b600541807dcb0ea9fe253e59f23ce36cc811d627acae5e2abdc00b7ed155f3e6b0f',
            '0203dd7138c7378444fe3c1b1572a351f88505aeab2d9f8ed4a8f67d66e76983072d8ae6e496b3953a8603543c2dc64749ee15fe3575e4505b502bfe696f06690e',
            '0287b572b6c096bc11a8c10fe1fc4ba2085633f8e1bdd2e39df8f46c9bf733ca068261d8006f22ee2bfaf4366e26d42b00befdddd9058a5c87a0f39c757f121909',
            '021e2ea38aa07601e07a3d7623a97e68d3251525304d2a748548c7b46d07c20b0c78506b19cae49d569d0a8c4979c74f7d8d19f7e595d307ddf00faf3d8f621c0d',
            '0214f758c8fb4a521a1e3d25b9fb535974f6aab1c1dda5988e986dda7e17140909a7b7bdb3d5e17a2ebd5deb3530d10c6f5d6966f525c1cbca408059949ff65304',
            '02f707c4a37066a692986ddfdd2ca71f68c6f45a956d45eaf6e8e7a2e5272ac3033eb26ca2b55bf86e90ab8ddcdbad88a82ded88deb552614190440169afcee004',
            '02edb8a5b8cc02a2e03b95ea068084ae2496f21d4dfd0842c63836137e37047b06d5a0160994396c98630d8b47878e9c18fea4fb824588c143e05c4b18bfea2301',
            '02aa59c2ef76ac97c261279a1c6ed3724d66a437fe8df0b85e8858703947a2b10f04e49912a0626c09849c3b4a3ea46166cd909b9fd561257730c91cbccf4abe07',
            '02c64a98c59c4a3d7c583de65404c5a54b350a25011dfca70cd84e3f6e570428026236028fce31bfd8d9fc5401867ab5349eb0859c65df05b380899a7bdfee9003',
            '03da465e27f7feec31353cb668f0e8965391f983b06c0684b35b00af38533603',
        ]

        mg_buff = [ubinascii.unhexlify(x) for x in mg_buff]
        mg_buff_b = list(mg_buff)
        mg_res = step_09_sign_input._protect_signature(st, mg_buff)

        iv = offloading_keys.key_signature(mst, st.current_input_index,
                                           True)[:12]
        key = offloading_keys.key_signature(mst, st.current_input_index, False)
        cipher = chacha20poly1305(key, iv)
        ciphertext = cipher.encrypt(b"".join(mg_buff_b))
        ciphertext += cipher.finish()
        self.assertEqual(b"".join(mg_res), ciphertext)

        cipher = chacha20poly1305(key, iv)
        ciphertext = b"".join(mg_res)
        exp_tag, ciphertext = ciphertext[-16:], ciphertext[:-16]
        plaintext = cipher.decrypt(ciphertext)
        tag = cipher.finish()
        self.assertEqual(tag, exp_tag)
        self.assertEqual(plaintext, b"".join(mg_buff_b))
Esempio n. 8
0
def _check_rsig_data(state: State, rsig_data: MoneroTransactionRsigData):
    """
    There are two types of monero ring confidential transactions:
    1. RCTTypeFull = 1 (used if num_inputs == 1)
    2. RCTTypeSimple = 2 (for num_inputs > 1)

    and four types of range proofs (set in `rsig_data.rsig_type`):
    1. RangeProofBorromean = 0
    2. RangeProofBulletproof = 1
    3. RangeProofMultiOutputBulletproof = 2
    4. RangeProofPaddedBulletproof = 3
    """
    state.rsig_grouping = rsig_data.grouping

    if rsig_data.rsig_type == 0:
        state.rsig_type = RsigType.Borromean
    elif rsig_data.rsig_type in (1, 2, 3):
        state.rsig_type = RsigType.Bulletproof
    else:
        raise ValueError("Unknown rsig type")

    # unintuitively RctType.Simple is used for more inputs
    if state.input_count > 1 or state.rsig_type == RsigType.Bulletproof:
        state.rct_type = RctType.Simple
    else:
        state.rct_type = RctType.Full

    if state.rsig_type == RsigType.Bulletproof and state.output_count > 2:
        state.rsig_offload = True

    _check_grouping(state)
async def init_transaction(
    state: State,
    address_n: list,
    network_type: int,
    tsx_data: MoneroTransactionData,
    keychain,
) -> MoneroTransactionInitAck:
    from apps.monero.signing import offloading_keys
    from apps.common import paths

    await paths.validate_path(state.ctx, misc.validate_full_path, keychain,
                              address_n, CURVE)

    state.creds = misc.get_creds(keychain, address_n, network_type)
    state.client_version = tsx_data.client_version or 0
    if state.client_version == 0:
        raise ValueError("Client version not supported")

    state.fee = state.fee if state.fee > 0 else 0
    state.tx_priv = crypto.random_scalar()
    state.tx_pub = crypto.scalarmult_base(state.tx_priv)
    state.mem_trace(1)

    state.input_count = tsx_data.num_inputs
    state.output_count = len(tsx_data.outputs)
    state.progress_total = 4 + 3 * state.input_count + state.output_count
    state.progress_cur = 0

    # Ask for confirmation
    await confirms.require_confirm_transaction(state.ctx, state, tsx_data,
                                               state.creds.network_type)
    state.creds.address = None
    state.creds.network_type = None
    gc.collect()
    state.mem_trace(3)

    # Basic transaction parameters
    state.output_change = tsx_data.change_dts
    state.mixin = tsx_data.mixin
    state.fee = tsx_data.fee
    state.account_idx = tsx_data.account
    state.last_step = state.STEP_INIT
    if tsx_data.hard_fork:
        state.hard_fork = tsx_data.hard_fork

    # Ensure change is correct
    _check_change(state, tsx_data.outputs)

    # At least two outpus are required, this applies also for sweep txs
    # where one fake output is added. See _check_change for more info
    if state.output_count < 2:
        raise signing.NotEnoughOutputsError(
            "At least two outputs are required")

    _check_rsig_data(state, tsx_data.rsig_data)
    _check_subaddresses(state, tsx_data.outputs)

    # Extra processing, payment id
    _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.uvarint(
        2)  # current Monero transaction format (RingCT = 2)
    state.tx_prefix_hasher.uvarint(tsx_data.unlock_time)
    state.tx_prefix_hasher.uvarint(state.input_count)  # ContainerType, size
    state.mem_trace(10, True)

    # Final message hasher
    state.full_message_hasher.init()
    state.full_message_hasher.set_type_fee(signing.RctType.Bulletproof2,
                                           state.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)

    # HMACs all outputs to disallow tampering.
    # Each HMAC is then sent alongside the output
    # and trezor validates it.
    hmacs = []
    for idx in range(state.output_count):
        c_hmac = await offloading_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)

    return MoneroTransactionInitAck(hmacs=hmacs, rsig_data=rsig_data)