diff --git a/Pipfile.lock b/Pipfile.lock index 882d57a1f..08435039e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -423,10 +423,9 @@ }, "py-cryptonight": { "hashes": [ - "sha256:da83570f040bb6c4caad05d28ca7ffc4a0c983a18c60c733366ae8858d587b1f", - "sha256:fd55a7915b68b7d46794de646d9bff7fb1870b905c83903e36e1b9cb50cc01d2" + "sha256:18ef26c56d7b677c527932af37a9bc98f39e4a56a87a003ef9a3a994d8bfe080" ], - "version": "==0.1.6" + "version": "==0.1.8" }, "py-trezor-crypto-ph4": { "hashes": [ diff --git a/src/apps/monero/__init__.py b/src/apps/monero/__init__.py index d45314065..e0754eeb2 100644 --- a/src/apps/monero/__init__.py +++ b/src/apps/monero/__init__.py @@ -15,6 +15,8 @@ def boot(): wire.add( MessageType.MoneroKeyImageExportInitRequest, __name__, "key_image_sync", ns ) + wire.add(MessageType.MoneroGetTxKeyRequest, __name__, "get_tx_keys", ns) + wire.add(MessageType.MoneroLiveRefreshStartRequest, __name__, "live_refresh", ns) if __debug__ and hasattr(MessageType, "DebugMoneroDiagRequest"): wire.add(MessageType.DebugMoneroDiagRequest, __name__, "diag") diff --git a/src/apps/monero/get_tx_keys.py b/src/apps/monero/get_tx_keys.py new file mode 100644 index 000000000..0e4a22a72 --- /dev/null +++ b/src/apps/monero/get_tx_keys.py @@ -0,0 +1,77 @@ +""" +This `get_tx_key` command supports retrieval of private tx keys (not spend keys, +just random transaction privates `r` and additional private keys if applicable) +required by users to check the transaction or when resolving disputes with +the recipient. + +It supports returning transaction derivations = private tx key * public view key. +This enables to compute the tx_proof for outgoing transactions which are also +a nice tool when resolving disputes, provides better protection as tx private +key are not exported in this case. + +This is related to singing/step10 where we send `tx_enc_keys` to the host +encrypted using the private spend key. Here the host sends it back +in `MoneroGetTxKeyRequest.tx_enc_keys` to be decrypted and yet again encrypted +using the view key, which the host possess. +""" + +from trezor import utils +from trezor.messages.MoneroGetTxKeyAck import MoneroGetTxKeyAck +from trezor.messages.MoneroGetTxKeyRequest import MoneroGetTxKeyRequest + +from apps.common import paths +from apps.monero import misc +from apps.monero.layout import confirms +from apps.monero.xmr import crypto +from apps.monero.xmr.crypto import chacha_poly + +_GET_TX_KEY_REASON_TX_KEY = 0 +_GET_TX_KEY_REASON_TX_DERIVATION = 1 + + +async def get_tx_keys(ctx, msg: MoneroGetTxKeyRequest, keychain): + await paths.validate_path(ctx, misc.validate_full_path, path=msg.address_n) + + do_deriv = msg.reason == _GET_TX_KEY_REASON_TX_DERIVATION + await confirms.require_confirm_tx_key(ctx, export_key=not do_deriv) + + creds = misc.get_creds(keychain, msg.address_n, msg.network_type) + + tx_enc_key = misc.compute_tx_key( + creds.spend_key_private, + msg.tx_prefix_hash, + msg.salt1, + crypto.decodeint(msg.salt2), + ) + + # the plain_buff first stores the tx_priv_keys as decrypted here + # and then is used to store the derivations if applicable + plain_buff = chacha_poly.decrypt_pack(tx_enc_key, msg.tx_enc_keys) + utils.ensure(len(plain_buff) % 32 == 0, "Tx key buffer has invalid size") + del msg.tx_enc_keys + + # If return only derivations do tx_priv * view_pub + if do_deriv: + plain_buff = bytearray(plain_buff) + view_pub = crypto.decodepoint(msg.view_public_key) + tx_priv = crypto.new_scalar() + derivation = crypto.new_point() + n_keys = len(plain_buff) // 32 + for c in range(n_keys): + crypto.decodeint_into(tx_priv, plain_buff, 32 * c) + crypto.scalarmult_into(derivation, view_pub, tx_priv) + crypto.encodepoint_into(plain_buff, derivation, 32 * c) + + # Encrypt by view-key based password. + tx_enc_key_host, salt = misc.compute_enc_key_host( + creds.view_key_private, msg.tx_prefix_hash + ) + + res = chacha_poly.encrypt_pack(tx_enc_key_host, plain_buff) + res_msg = MoneroGetTxKeyAck(salt=salt) + if do_deriv: + res_msg.tx_derivations = res + return res_msg + + res_msg.tx_keys = res + return res_msg diff --git a/src/apps/monero/layout/confirms.py b/src/apps/monero/layout/confirms.py index 6a0284514..56fc81d2c 100644 --- a/src/apps/monero/layout/confirms.py +++ b/src/apps/monero/layout/confirms.py @@ -21,6 +21,25 @@ async def require_confirm_keyimage_sync(ctx): return await require_confirm(ctx, content, ButtonRequestType.SignTx) +async def require_confirm_live_refresh(ctx): + content = Text("Confirm ki sync", ui.ICON_SEND, icon_color=ui.GREEN) + content.normal("Do you really want to", "start refresh?") + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_tx_key(ctx, export_key=False): + content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN) + txt = ["Do you really want to"] + if export_key: + txt.append("export tx_key?") + else: + txt.append("export tx_der") + txt.append("for tx_proof?") + + content.normal(*txt) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + async def require_confirm_transaction(ctx, tsx_data, network_type): """ Ask for confirmation from user. @@ -132,3 +151,12 @@ async def keyimage_sync_step(ctx, current, total_num): text = Text("Syncing", ui.ICON_SEND, icon_color=ui.BLUE) text.normal("%d/%d" % (current + 1, total_num)) text.render() + + +@ui.layout +async def live_refresh_step(ctx, current): + if current is None: + return + text = Text("Refreshing", ui.ICON_SEND, icon_color=ui.BLUE) + text.normal("%d" % current) + text.render() diff --git a/src/apps/monero/live_refresh.py b/src/apps/monero/live_refresh.py new file mode 100644 index 000000000..0deecbafc --- /dev/null +++ b/src/apps/monero/live_refresh.py @@ -0,0 +1,91 @@ +import gc + +from trezor import log +from trezor.messages import MessageType +from trezor.messages.MoneroLiveRefreshFinalAck import MoneroLiveRefreshFinalAck +from trezor.messages.MoneroLiveRefreshStartAck import MoneroLiveRefreshStartAck +from trezor.messages.MoneroLiveRefreshStartRequest import MoneroLiveRefreshStartRequest +from trezor.messages.MoneroLiveRefreshStepAck import MoneroLiveRefreshStepAck +from trezor.messages.MoneroLiveRefreshStepRequest import MoneroLiveRefreshStepRequest + +from apps.common import paths +from apps.monero import misc +from apps.monero.layout import confirms +from apps.monero.xmr import crypto, key_image, monero +from apps.monero.xmr.crypto import chacha_poly + + +async def live_refresh(ctx, msg: MoneroLiveRefreshStartRequest, keychain): + state = LiveRefreshState() + + res = await _init_step(state, ctx, msg, keychain) + while True: + msg = await ctx.call( + res, + MessageType.MoneroLiveRefreshStepRequest, + MessageType.MoneroLiveRefreshFinalRequest, + ) + del res + if msg.MESSAGE_WIRE_TYPE == MessageType.MoneroLiveRefreshStepRequest: + res = await _refresh_step(state, ctx, msg) + else: + return MoneroLiveRefreshFinalAck() + gc.collect() + + return res + + +class LiveRefreshState: + def __init__(self): + self.current_output = -1 + self.creds = None + + +async def _init_step( + s: LiveRefreshState, ctx, msg: MoneroLiveRefreshStartRequest, keychain +): + await paths.validate_path(ctx, misc.validate_full_path, path=msg.address_n) + + await confirms.require_confirm_live_refresh(ctx) + + s.creds = misc.get_creds(keychain, msg.address_n, msg.network_type) + + return MoneroLiveRefreshStartAck() + + +async def _refresh_step(s: LiveRefreshState, ctx, msg: MoneroLiveRefreshStepRequest): + buff = bytearray(32 * 3) + buff_mv = memoryview(buff) + + await confirms.live_refresh_step(ctx, s.current_output) + s.current_output += 1 + + if __debug__: + log.debug(__name__, "refresh, step i: %d", s.current_output) + + # Compute spending secret key and the key image + # spend_priv = Hs(recv_deriv || real_out_idx) + spend_key_private + # If subaddr: + # spend_priv += Hs("SubAddr" || view_key_private || major || minor) + # out_key = spend_priv * G, KI: spend_priv * Hp(out_key) + out_key = crypto.decodepoint(msg.out_key) + recv_deriv = crypto.decodepoint(msg.recv_deriv) + received_index = msg.sub_addr_major, msg.sub_addr_minor + spend_priv, ki = monero.generate_tx_spend_and_key_image( + s.creds, out_key, recv_deriv, msg.real_out_idx, received_index + ) + + ki_enc = crypto.encodepoint(ki) + sig = key_image.generate_ring_signature(ki_enc, ki, [out_key], spend_priv, 0, False) + del spend_priv # spend_priv never leaves the device + + # Serialize into buff + buff[0:32] = ki_enc + crypto.encodeint_into(buff_mv[32:64], sig[0][0]) + crypto.encodeint_into(buff_mv[64:], sig[0][1]) + + # Encrypt with view key private based key - so host can decrypt and verify HMAC + enc_key, salt = misc.compute_enc_key_host(s.creds.view_key_private, msg.out_key) + resp = chacha_poly.encrypt_pack(enc_key, buff) + + return MoneroLiveRefreshStepAck(salt=salt, key_image=resp) diff --git a/src/apps/monero/misc.py b/src/apps/monero/misc.py index 2e5cd0474..c44d20110 100644 --- a/src/apps/monero/misc.py +++ b/src/apps/monero/misc.py @@ -1,5 +1,8 @@ from apps.common import HARDENED +if False: + from apps.monero.xmr.types import * + def get_creds(keychain, address_n=None, network_type=None): from apps.monero.xmr import crypto, monero @@ -37,3 +40,28 @@ def validate_full_path(path: list) -> bool: if path[2] < HARDENED or path[2] > 1000000 | HARDENED: return False return True + + +def compute_tx_key( + spend_key_private: Sc25519, + tx_prefix_hash: bytes, + salt: bytes, + rand_mult_num: Sc25519, +) -> bytes: + from apps.monero.xmr import crypto + + rand_inp = crypto.sc_add(spend_key_private, rand_mult_num) + passwd = crypto.keccak_2hash(crypto.encodeint(rand_inp) + tx_prefix_hash) + tx_key = crypto.compute_hmac(salt, passwd) + return tx_key + + +def compute_enc_key_host( + view_key_private: Sc25519, tx_prefix_hash: bytes +) -> Tuple[bytes, bytes]: + from apps.monero.xmr import crypto + + salt = crypto.random_bytes(32) + passwd = crypto.keccak_2hash(crypto.encodeint(view_key_private) + tx_prefix_hash) + tx_key = crypto.compute_hmac(salt, passwd) + return tx_key, salt diff --git a/src/apps/monero/signing/step_10_sign_final.py b/src/apps/monero/signing/step_10_sign_final.py index 441f9f669..ba7aa3721 100644 --- a/src/apps/monero/signing/step_10_sign_final.py +++ b/src/apps/monero/signing/step_10_sign_final.py @@ -12,6 +12,7 @@ from trezor.messages.MoneroTransactionFinalAck import MoneroTransactionFinalAck from .state import State +from apps.monero import misc from apps.monero.xmr import crypto from apps.monero.xmr.crypto import chacha_poly @@ -37,7 +38,5 @@ def _compute_tx_key(spend_key_private, tx_prefix_hash): rand_mult_num = crypto.random_scalar() rand_mult = crypto.encodeint(rand_mult_num) - rand_inp = crypto.sc_add(spend_key_private, rand_mult_num) - passwd = crypto.keccak_2hash(crypto.encodeint(rand_inp) + tx_prefix_hash) - tx_key = crypto.compute_hmac(salt, passwd) + tx_key = misc.compute_tx_key(spend_key_private, tx_prefix_hash, salt, rand_mult_num) return tx_key, salt, rand_mult diff --git a/src/apps/monero/xmr/key_image.py b/src/apps/monero/xmr/key_image.py index 5522f6685..8d774c634 100644 --- a/src/apps/monero/xmr/key_image.py +++ b/src/apps/monero/xmr/key_image.py @@ -40,12 +40,12 @@ def _export_key_image( xi, ki, recv_derivation = r[:3] phash = crypto.encodepoint(ki) - sig = _generate_ring_signature(phash, ki, [pkey], xi, 0, test) + sig = generate_ring_signature(phash, ki, [pkey], xi, 0, test) return ki, sig -def _generate_ring_signature(prefix_hash, image, pubs, sec, sec_idx, test=False): +def generate_ring_signature(prefix_hash, image, pubs, sec, sec_idx, test=False): """ Generates ring signature with key image. void crypto_ops::generate_ring_signature() diff --git a/tests/run_tests_device_emu_monero.sh b/tests/run_tests_device_emu_monero.sh index a8cff51bc..556f9e9ab 100755 --- a/tests/run_tests_device_emu_monero.sh +++ b/tests/run_tests_device_emu_monero.sh @@ -16,6 +16,8 @@ cd .. export EC_BACKEND_FORCE=1 export EC_BACKEND=1 +export TREZOR_TEST_GET_TX=1 +export TREZOR_TEST_LIVE_REFRESH=1 python3 -m unittest trezor_monero_test.test_trezor error=$? kill $upy_pid