mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-18 05:28:40 +00:00
Merge pull request #442 from ph4r05/ph4r05/xmr-get-txkey
xmr: get_tx_keys, live_refresh
This commit is contained in:
commit
e81f89288f
5
Pipfile.lock
generated
5
Pipfile.lock
generated
@ -423,10 +423,9 @@
|
|||||||
},
|
},
|
||||||
"py-cryptonight": {
|
"py-cryptonight": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:da83570f040bb6c4caad05d28ca7ffc4a0c983a18c60c733366ae8858d587b1f",
|
"sha256:18ef26c56d7b677c527932af37a9bc98f39e4a56a87a003ef9a3a994d8bfe080"
|
||||||
"sha256:fd55a7915b68b7d46794de646d9bff7fb1870b905c83903e36e1b9cb50cc01d2"
|
|
||||||
],
|
],
|
||||||
"version": "==0.1.6"
|
"version": "==0.1.8"
|
||||||
},
|
},
|
||||||
"py-trezor-crypto-ph4": {
|
"py-trezor-crypto-ph4": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -15,6 +15,8 @@ def boot():
|
|||||||
wire.add(
|
wire.add(
|
||||||
MessageType.MoneroKeyImageExportInitRequest, __name__, "key_image_sync", ns
|
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"):
|
if __debug__ and hasattr(MessageType, "DebugMoneroDiagRequest"):
|
||||||
wire.add(MessageType.DebugMoneroDiagRequest, __name__, "diag")
|
wire.add(MessageType.DebugMoneroDiagRequest, __name__, "diag")
|
||||||
|
77
src/apps/monero/get_tx_keys.py
Normal file
77
src/apps/monero/get_tx_keys.py
Normal file
@ -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
|
@ -21,6 +21,25 @@ async def require_confirm_keyimage_sync(ctx):
|
|||||||
return await require_confirm(ctx, content, ButtonRequestType.SignTx)
|
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):
|
async def require_confirm_transaction(ctx, tsx_data, network_type):
|
||||||
"""
|
"""
|
||||||
Ask for confirmation from user.
|
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 = Text("Syncing", ui.ICON_SEND, icon_color=ui.BLUE)
|
||||||
text.normal("%d/%d" % (current + 1, total_num))
|
text.normal("%d/%d" % (current + 1, total_num))
|
||||||
text.render()
|
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()
|
||||||
|
91
src/apps/monero/live_refresh.py
Normal file
91
src/apps/monero/live_refresh.py
Normal file
@ -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)
|
@ -1,5 +1,8 @@
|
|||||||
from apps.common import HARDENED
|
from apps.common import HARDENED
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from apps.monero.xmr.types import *
|
||||||
|
|
||||||
|
|
||||||
def get_creds(keychain, address_n=None, network_type=None):
|
def get_creds(keychain, address_n=None, network_type=None):
|
||||||
from apps.monero.xmr import crypto, monero
|
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:
|
if path[2] < HARDENED or path[2] > 1000000 | HARDENED:
|
||||||
return False
|
return False
|
||||||
return True
|
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
|
||||||
|
@ -12,6 +12,7 @@ from trezor.messages.MoneroTransactionFinalAck import MoneroTransactionFinalAck
|
|||||||
|
|
||||||
from .state import State
|
from .state import State
|
||||||
|
|
||||||
|
from apps.monero import misc
|
||||||
from apps.monero.xmr import crypto
|
from apps.monero.xmr import crypto
|
||||||
from apps.monero.xmr.crypto import chacha_poly
|
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_num = crypto.random_scalar()
|
||||||
rand_mult = crypto.encodeint(rand_mult_num)
|
rand_mult = crypto.encodeint(rand_mult_num)
|
||||||
|
|
||||||
rand_inp = crypto.sc_add(spend_key_private, rand_mult_num)
|
tx_key = misc.compute_tx_key(spend_key_private, tx_prefix_hash, salt, rand_mult_num)
|
||||||
passwd = crypto.keccak_2hash(crypto.encodeint(rand_inp) + tx_prefix_hash)
|
|
||||||
tx_key = crypto.compute_hmac(salt, passwd)
|
|
||||||
return tx_key, salt, rand_mult
|
return tx_key, salt, rand_mult
|
||||||
|
@ -40,12 +40,12 @@ def _export_key_image(
|
|||||||
xi, ki, recv_derivation = r[:3]
|
xi, ki, recv_derivation = r[:3]
|
||||||
|
|
||||||
phash = crypto.encodepoint(ki)
|
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
|
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.
|
Generates ring signature with key image.
|
||||||
void crypto_ops::generate_ring_signature()
|
void crypto_ops::generate_ring_signature()
|
||||||
|
@ -16,6 +16,8 @@ cd ..
|
|||||||
|
|
||||||
export EC_BACKEND_FORCE=1
|
export EC_BACKEND_FORCE=1
|
||||||
export EC_BACKEND=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
|
python3 -m unittest trezor_monero_test.test_trezor
|
||||||
error=$?
|
error=$?
|
||||||
kill $upy_pid
|
kill $upy_pid
|
||||||
|
Loading…
Reference in New Issue
Block a user