def test_scalarmult_base(self): scalar = crypto.decodeint( unhexlify( b"a0eea49140a3b036da30eacf64bd9d56ce3ef68ba82ef13571ec511edbcf8303" ) ) exp = unhexlify( b"16bb4a3c44e2ced511fc0d4cd86b13b3af21efc99fb0356199fac489f2544c09" ) res = crypto.scalarmult_base(scalar) self.assertEqual(exp, crypto.encodepoint(res)) self.assertTrue(crypto.point_eq(crypto.decodepoint(exp), res)) scalar = crypto.decodeint( unhexlify( b"fd290dce39f781aebbdbd24584ed6d48bd300de19d9c3decfda0a6e2c6751d0f" ) ) exp = unhexlify( b"123daf90fc26f13c6529e6b49bfed498995ac383ef19c0db6771143f24ba8dd5" ) res = crypto.scalarmult_base(scalar) self.assertEqual(exp, crypto.encodepoint(res)) self.assertTrue(crypto.point_eq(crypto.decodepoint(exp), res))
def generate_mlsag_simple(message, pubs, in_sk, a, cout, kLRki, index): """ MLSAG for RctType.Simple :param message: the full message to be signed (actually its hash) :param pubs: vector of MoneroRctKey; this forms the ring; point values in encoded form; (dest, mask) = (P, C) :param in_sk: CtKey; spending private key with input commitment mask (original); better_name: input_secret_key :param a: mask from the pseudo output commitment; better name: pseudo_out_alpha :param cout: pseudo output commitment; point, decoded; better name: pseudo_out_c :param kLRki: used only in multisig, currently not implemented :param index: specifies corresponding public key to the `in_sk` in the pubs array :return: MgSig """ # Monero signs inputs separately, so `rows` always equals 2 (pubkey, commitment) # and `dsRows` is always 1 (denotes where the pubkeys "end") rows = 2 dsRows = 1 cols = len(pubs) if cols == 0: raise ValueError("Empty pubs") sk = _key_vector(rows) M = _key_matrix(rows, cols) sk[0] = in_sk.dest sk[1] = crypto.sc_sub(in_sk.mask, a) for i in range(cols): M[i][0] = crypto.decodepoint(pubs[i].dest) M[i][1] = crypto.point_sub(crypto.decodepoint(pubs[i].commitment), cout) return generate_mlsag(message, M, sk, kLRki, index, dsRows)
def export_key_image(creds: AccountCreds, subaddresses: Subaddresses, td: MoneroTransferDetails) -> tuple[Ge25519, Sig]: out_key = crypto.decodepoint(td.out_key) tx_pub_key = crypto.decodepoint(td.tx_pub_key) additional_tx_pub_key = None if len(td.additional_tx_pub_keys) == 1: # compression additional_tx_pub_key = crypto.decodepoint( td.additional_tx_pub_keys[0]) elif td.additional_tx_pub_keys: if td.internal_output_index >= len(td.additional_tx_pub_keys): raise ValueError("Wrong number of additional derivations") additional_tx_pub_key = crypto.decodepoint( td.additional_tx_pub_keys[td.internal_output_index]) ki, sig = _export_key_image( creds, subaddresses, out_key, tx_pub_key, additional_tx_pub_key, td.internal_output_index, True, td.sub_addr_major, td.sub_addr_minor, ) return ki, sig
def gen_clsag_sig(self, ring_size=11, index=None): msg = random.bytes(32) amnt = crypto.sc_init(random.uniform(0xFFFFFF) + 12) priv = crypto.random_scalar() msk = crypto.random_scalar() alpha = crypto.random_scalar() P = crypto.scalarmult_base(priv) C = crypto.add_keys2(msk, amnt, crypto.xmr_H()) Cp = crypto.add_keys2(alpha, amnt, crypto.xmr_H()) ring = [] for i in range(ring_size - 1): tk = TmpKey( crypto.encodepoint( crypto.scalarmult_base(crypto.random_scalar())), crypto.encodepoint( crypto.scalarmult_base(crypto.random_scalar())), ) ring.append(tk) index = index if index is not None else random.uniform(len(ring)) ring.insert(index, TmpKey(crypto.encodepoint(P), crypto.encodepoint(C))) ring2 = list(ring) mg_buffer = [] self.assertTrue( crypto.point_eq(crypto.scalarmult_base(priv), crypto.decodepoint(ring[index].dest))) self.assertTrue( crypto.point_eq( crypto.scalarmult_base(crypto.sc_sub(msk, alpha)), crypto.point_sub(crypto.decodepoint(ring[index].commitment), Cp), )) mlsag.generate_clsag_simple( msg, ring, CtKey(priv, msk), alpha, Cp, index, mg_buffer, ) sD = crypto.decodepoint(mg_buffer[-1]) sc1 = crypto.decodeint(mg_buffer[-2]) scalars = [crypto.decodeint(x) for x in mg_buffer[1:-2]] H = crypto.new_point() sI = crypto.new_point() crypto.hash_to_point_into(H, crypto.encodepoint(P)) crypto.scalarmult_into(sI, H, priv) # I = p*H return msg, scalars, sc1, sI, sD, ring2, Cp
def test_encoding(self): point = unhexlify( b"2486224797d05cae3cba4be043be2db0df381f3f19cfa113f86ab38e3d8d2bd0" ) self.assertEqual(point, crypto.encodepoint(crypto.decodepoint(point))) self.assertTrue( crypto.point_eq( crypto.decodepoint(point), crypto.decodepoint( crypto.encodepoint(crypto.decodepoint(point))), ))
def test_public_spend(self): derivation = unhexlify( b"e720a09f2e3a0bbf4e4ba7ad93653bb296885510121f806acb2a5f9168fafa01" ) base = unhexlify( b"7d996b0f2db6dbb5f2a086211f2399a4a7479b2c911af307fdc3f7f61a88cb0e" ) pkey_ex = unhexlify( b"0846cae7405077b6b7800f0b932c10a186448370b6db318f8c9e13f781dab546" ) pkey_comp = crypto.derive_public_key(crypto.decodepoint(derivation), 0, crypto.decodepoint(base)) self.assertEqual(pkey_ex, crypto.encodepoint(pkey_comp))
def test_scalarmult(self): priv = unhexlify( b"3482fb9735ef879fcae5ec7721b5d3646e155c4fb58d6cc11c732c9c9b76620a" ) pub = unhexlify( b"2486224797d05cae3cba4be043be2db0df381f3f19cfa113f86ab38e3d8d2bd0" ) exp = unhexlify( b"adcd1f5881f46f254900a03c654e71950a88a0236fa0a3a946c9b8daed6ef43d" ) res = crypto.scalarmult(crypto.decodepoint(pub), crypto.decodeint(priv)) self.assertEqual(exp, crypto.encodepoint(res)) self.assertTrue(crypto.point_eq(crypto.decodepoint(exp), res))
def _get_additional_public_key( src_entr: MoneroTransactionSourceEntry, ) -> Ge25519 | None: additional_tx_pub_key = None if len(src_entr.real_out_additional_tx_keys) == 1: # compression additional_tx_pub_key = crypto.decodepoint( src_entr.real_out_additional_tx_keys[0]) elif src_entr.real_out_additional_tx_keys: if src_entr.real_output_in_tx_index >= len( src_entr.real_out_additional_tx_keys): raise ValueError("Wrong number of additional derivations") additional_tx_pub_key = crypto.decodepoint( src_entr.real_out_additional_tx_keys[ src_entr.real_output_in_tx_index]) return additional_tx_pub_key
def generate_mlsag_full(message, pubs, in_sk, out_sk_mask, out_pk_commitments, kLRki, index, txn_fee_key): cols = len(pubs) if cols == 0: raise ValueError("Empty pubs") rows = len(pubs[0]) if rows == 0: raise ValueError("Empty pub row") for i in range(cols): if len(pubs[i]) != rows: raise ValueError("pub is not rectangular") if len(in_sk) != rows: raise ValueError("Bad inSk size") if len(out_sk_mask) != len(out_pk_commitments): raise ValueError("Bad outsk/putpk size") sk = _key_vector(rows + 1) M = _key_matrix(rows + 1, cols) for i in range(rows + 1): sk[i] = crypto.sc_0() for i in range(cols): M[i][rows] = crypto.identity() for j in range(rows): M[i][j] = crypto.decodepoint(pubs[i][j].dest) M[i][rows] = crypto.point_add( M[i][rows], crypto.decodepoint(pubs[i][j].commitment)) sk[rows] = crypto.sc_0() for j in range(rows): sk[j] = in_sk[j].dest sk[rows] = crypto.sc_add(sk[rows], in_sk[j].mask) # add masks in last row for i in range(cols): for j in range(len(out_pk_commitments)): M[i][rows] = crypto.point_sub( M[i][rows], crypto.decodepoint( out_pk_commitments[j])) # subtract output Ci's in last row # Subtract txn fee output in last row M[i][rows] = crypto.point_sub(M[i][rows], txn_fee_key) for j in range(len(out_pk_commitments)): sk[rows] = crypto.sc_sub( sk[rows], out_sk_mask[j]) # subtract output masks in last row return generate_mlsag(message, M, sk, kLRki, index, rows)
def export_key_image(creds, subaddresses, td): out_key = crypto.decodepoint(td.out_key) tx_pub_key = crypto.decodepoint(td.tx_pub_key) additional_tx_pub_keys = [ crypto.decodepoint(x) for x in td.additional_tx_pub_keys ] ki, sig = _export_key_image( creds, subaddresses, out_key, tx_pub_key, additional_tx_pub_keys, td.internal_output_index, ) return ki, sig
def verify_monero_generated(self, clsag): msg = ubinascii.unhexlify(clsag["msg"]) sI = crypto.decodepoint(ubinascii.unhexlify(clsag["sI"])) sD = crypto.decodepoint(ubinascii.unhexlify(clsag["sD"])) sc1 = crypto.decodeint(ubinascii.unhexlify(clsag["sc1"])) Cout = crypto.decodepoint(ubinascii.unhexlify(clsag["cout"])) scalars = [ crypto.decodeint(ubinascii.unhexlify(x)) for x in clsag["ss"] ] ring = [] for e in clsag["ring"]: ring.append( TmpKey(ubinascii.unhexlify(e[0]), ubinascii.unhexlify(e[1]))) self.verify_clsag(msg, scalars, sc1, sI, sD, ring, Cout)
def test_clsag_invalid_P(self): res = self.gen_clsag_sig(ring_size=11, index=5) msg, scalars, sc1, sI, sD, ring2, Cp = res with self.assertRaises(ValueError): ring2[5].commitment = crypto.encodepoint( crypto.point_mul8(crypto.decodepoint(ring2[5].dest))) self.verify_clsag(msg, scalars, sc1, sI, sD, ring2, Cp)
def _compute_tx_keys( state: State, dst_entr: MoneroTransactionDestinationEntry ) -> Tuple[Ge25519, Sc25519]: """Computes tx_out_key, amount_key""" if state.is_processing_offloaded: return None, None # no need to recompute # additional tx key if applicable additional_txkey_priv = _set_out_additional_keys(state, dst_entr) # derivation = a*R or r*A or s*C derivation = _set_out_derivation(state, dst_entr, additional_txkey_priv) # amount key = H_s(derivation || i) amount_key = crypto.derivation_to_scalar(derivation, state.current_output_index) # one-time destination address P = H_s(derivation || i)*G + B tx_out_key = crypto.derive_public_key( derivation, state.current_output_index, crypto.decodepoint(dst_entr.addr.spend_public_key), ) del (derivation, additional_txkey_priv) from apps.monero.xmr import monero mask = monero.commitment_mask(crypto.encodeint(amount_key)) state.output_masks.append(mask) return tx_out_key, amount_key
def _set_out_derivation( state: State, dst_entr: MoneroTransactionDestinationEntry, additional_txkey_priv: Sc25519, ) -> Ge25519: """ Calculates derivation which is then used in the one-time address as `P = H(derivation)*G + B`. For change outputs the derivation equals a*R, because we know the private view key. For others it is either `r*A` for traditional addresses, or `s*C` for subaddresses. Both `r` and `s` are random scalars, `s` is used in the context of subaddresses, but it's basically the same thing. """ from apps.monero.xmr.addresses 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) if dst_entr.is_subaddress and state.need_additional_txkeys: deriv_priv = additional_txkey_priv else: deriv_priv = state.tx_priv derivation = crypto.generate_key_derivation( crypto.decodepoint(dst_entr.addr.view_public_key), deriv_priv) return derivation
def _set_out_additional_keys( state: State, dst_entr: MoneroTransactionDestinationEntry ) -> Sc25519: """ If needed (decided in step 1), additional tx keys are calculated for this particular output. """ if not state.need_additional_txkeys: return None additional_txkey_priv = crypto.random_scalar() if dst_entr.is_subaddress: # R=r*D additional_txkey = crypto.decodepoint(dst_entr.addr.spend_public_key) crypto.scalarmult_into( additional_txkey, additional_txkey, additional_txkey_priv ) else: # R=r*G additional_txkey = crypto.scalarmult_base(additional_txkey_priv) state.additional_tx_public_keys.append(crypto.encodepoint(additional_txkey)) state.additional_tx_private_keys.append(additional_txkey_priv) return additional_txkey_priv
def test_derive_subaddress_public_key(self): out_key = crypto.decodepoint( unhexlify( b"f4efc29da4ccd6bc6e81f52a6f47b2952966442a7efb49901cce06a7a3bef3e5" )) deriv = crypto.decodepoint( unhexlify( b"259ef2aba8feb473cf39058a0fe30b9ff6d245b42b6826687ebd6b63128aff64" )) res = crypto.encodepoint( monero.derive_subaddress_public_key(out_key, deriv, 5)) self.assertEqual( res, unhexlify( b"5a10cca900ee47a7f412cd661b29f5ab356d6a1951884593bb170b5ec8b6f2e8" ), )
def test_derivation_to_scalar(self): derivation = unhexlify( b"e720a09f2e3a0bbf4e4ba7ad93653bb296885510121f806acb2a5f9168fafa01" ) scalar = unhexlify( b"25d08763414c379aa9cf989cdcb3cadd36bd5193b500107d6bf5f921f18e470e" ) sc_int = crypto.derivation_to_scalar(crypto.decodepoint(derivation), 0) self.assertEqual(scalar, crypto.encodeint(sc_int))
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 _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)
async def get_tx_keys(ctx, msg: MoneroGetTxKeyRequest, keychain): await paths.validate_path( ctx, misc.validate_full_path, keychain, msg.address_n, CURVE ) 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
def test_generate_key_derivation(self): key_pub = crypto.decodepoint( unhexlify( b"7739c95d3298e2f87362dba9e0e0b3980a692ae8e2f16796b0e382098cd6bd83" )) key_priv = crypto.decodeint( unhexlify( b"3482fb9735ef879fcae5ec7721b5d3646e155c4fb58d6cc11c732c9c9b76620a" )) deriv_exp = unhexlify( b"fa188a45a0e4daccc0e6d4f6f6858fd46392104be74183ec0047e7e9f4eaf739" ) self.assertEqual( deriv_exp, crypto.encodepoint( crypto.generate_key_derivation(key_pub, key_priv)), )
def _check_subaddresses(state: State, outputs: list): """ 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( crypto.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 _compute_tx_keys(state: State, dst_entr): """Computes tx_out_key, amount_key""" if state.is_processing_offloaded: return None, None # no need to recompute # additional tx key if applicable additional_txkey_priv = _set_out_additional_keys(state, dst_entr) # derivation = a*R or r*A or s*C derivation = _set_out_derivation(state, dst_entr, additional_txkey_priv) # amount key = H_s(derivation || i) amount_key = crypto.derivation_to_scalar(derivation, state.current_output_index) # one-time destination address P = H_s(derivation || i)*G + B tx_out_key = crypto.derive_public_key( derivation, state.current_output_index, crypto.decodepoint(dst_entr.addr.spend_public_key), ) del (derivation, additional_txkey_priv) # Computes the newest mask if applicable if state.is_det_mask(): from apps.monero.xmr import monero mask = monero.commitment_mask(crypto.encodeint(amount_key)) elif state.current_output_index + 1 < state.output_count: mask = offloading_keys.det_comm_masks(state.key_enc, state.current_output_index) else: mask = state.output_last_mask state.output_last_mask = None state.output_masks.append(mask) return tx_out_key, amount_key
async def sign_input( state: State, src_entr: MoneroTransactionSourceEntry, vini_bin: bytes, vini_hmac: bytes, pseudo_out: bytes, pseudo_out_hmac: bytes, pseudo_out_alpha_enc: bytes, spend_enc: bytes, orig_idx: int, ) -> MoneroTransactionSignInputAck: """ :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 vini_hmac: HMAC for the tx.vin[i] as returned from Trezor :param pseudo_out: Pedersen commitment for the current input, uses pseudo_out_alpha as a mask. Only applicable for RCTTypeSimple. :param pseudo_out_hmac: HMAC for pseudo_out :param pseudo_out_alpha_enc: alpha mask used in pseudo_out, only applicable for RCTTypeSimple. Encrypted. :param spend_enc: one time address spending private key. Encrypted. :param orig_idx: original index of the src_entr before sorting (HMAC check) :return: Generated signature MGs[i] """ await confirms.transaction_step(state, state.STEP_SIGN, state.current_input_index + 1) state.current_input_index += 1 if state.last_step not in (state.STEP_ALL_OUT, state.STEP_SIGN): raise ValueError("Invalid state transition") if state.current_input_index >= state.input_count: raise ValueError("Invalid inputs count") if pseudo_out is None: raise ValueError("SimpleRCT requires pseudo_out but none provided") if pseudo_out_alpha_enc is None: raise ValueError( "SimpleRCT requires pseudo_out's mask but none provided") input_position = (state.source_permutation[state.current_input_index] if state.client_version <= 1 else orig_idx) mods = utils.unimport_begin() # Check input's HMAC from apps.monero.signing import offloading_keys vini_hmac_comp = await offloading_keys.gen_hmac_vini( state.key_hmac, src_entr, vini_bin, input_position) if not crypto.ct_equals(vini_hmac_comp, vini_hmac): raise ValueError("HMAC is not correct") # Key image sorting check - permutation correctness cur_ki = offloading_keys.get_ki_from_vini(vini_bin) if state.current_input_index > 0 and state.last_ki <= cur_ki: raise ValueError("Key image order invalid") state.last_ki = cur_ki if state.current_input_index < state.input_count else None del (cur_ki, vini_bin, vini_hmac, vini_hmac_comp) gc.collect() state.mem_trace(1, True) from apps.monero.xmr.crypto import chacha_poly pseudo_out_alpha = crypto.decodeint( chacha_poly.decrypt_pack( offloading_keys.enc_key_txin_alpha(state.key_enc, input_position), bytes(pseudo_out_alpha_enc), )) # Last pseudo_out is recomputed so mask sums hold if input_position + 1 == state.input_count: # Recompute the lash alpha so the sum holds state.mem_trace("Correcting alpha") alpha_diff = crypto.sc_sub(state.sumout, state.sumpouts_alphas) crypto.sc_add_into(pseudo_out_alpha, pseudo_out_alpha, alpha_diff) pseudo_out_c = crypto.gen_commitment(pseudo_out_alpha, state.input_last_amount) else: if input_position + 1 == state.input_count: utils.ensure(crypto.sc_eq(state.sumpouts_alphas, state.sumout), "Sum eq error") # both pseudo_out and its mask were offloaded so we need to # validate pseudo_out's HMAC and decrypt the alpha pseudo_out_hmac_comp = crypto.compute_hmac( offloading_keys.hmac_key_txin_comm(state.key_hmac, input_position), pseudo_out, ) if not crypto.ct_equals(pseudo_out_hmac_comp, pseudo_out_hmac): raise ValueError("HMAC is not correct") pseudo_out_c = crypto.decodepoint(pseudo_out) state.mem_trace(2, True) # Spending secret spend_key = crypto.decodeint( chacha_poly.decrypt_pack( offloading_keys.enc_key_spend(state.key_enc, input_position), bytes(spend_enc), )) del ( offloading_keys, chacha_poly, pseudo_out, pseudo_out_hmac, pseudo_out_alpha_enc, spend_enc, ) utils.unimport_end(mods) state.mem_trace(3, True) # Basic setup, sanity check from apps.monero.xmr.serialize_messages.tx_ct_key import CtKey index = src_entr.real_output input_secret_key = CtKey(spend_key, crypto.decodeint(src_entr.mask)) # Private key correctness test utils.ensure( crypto.point_eq( crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.dest), crypto.scalarmult_base(input_secret_key.dest), ), "Real source entry's destination does not equal spend key's", ) utils.ensure( crypto.point_eq( crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.commitment), crypto.gen_commitment(input_secret_key.mask, src_entr.amount), ), "Real source entry's mask does not equal spend key's", ) state.mem_trace(4, True) from apps.monero.xmr import mlsag mg_buffer = [] ring_pubkeys = [x.key for x in src_entr.outputs if x] utils.ensure(len(ring_pubkeys) == len(src_entr.outputs), "Invalid ring") del src_entr state.mem_trace(5, True) if state.hard_fork and state.hard_fork >= 13: state.mem_trace("CLSAG") mlsag.generate_clsag_simple( state.full_message, ring_pubkeys, input_secret_key, pseudo_out_alpha, pseudo_out_c, index, mg_buffer, ) else: mlsag.generate_mlsag_simple( state.full_message, ring_pubkeys, input_secret_key, pseudo_out_alpha, pseudo_out_c, index, mg_buffer, ) del (CtKey, input_secret_key, pseudo_out_alpha, mlsag, ring_pubkeys) state.mem_trace(6, True) from trezor.messages.MoneroTransactionSignInputAck import ( MoneroTransactionSignInputAck, ) # Encrypt signature, reveal once protocol finishes OK if state.client_version >= 3: utils.unimport_end(mods) state.mem_trace(7, True) mg_buffer = _protect_signature(state, mg_buffer) state.mem_trace(8, True) state.last_step = state.STEP_SIGN return MoneroTransactionSignInputAck( signature=mg_buffer, pseudo_out=crypto.encodepoint(pseudo_out_c))
async def set_input( state: State, src_entr: MoneroTransactionSourceEntry ) -> MoneroTransactionSetInputAck: from trezor.messages import MoneroTransactionSetInputAck from apps.monero.xmr.crypto import chacha_poly from apps.monero.xmr.serialize_messages.tx_prefix import TxinToKey from apps.monero.signing import offloading_keys state.current_input_index += 1 await layout.transaction_step(state, state.STEP_INP, state.current_input_index) if state.last_step > state.STEP_INP: raise ValueError("Invalid state transition") if state.current_input_index >= state.input_count: raise ValueError("Too many inputs") # real_output denotes which output in outputs is the real one (ours) if src_entr.real_output >= len(src_entr.outputs): raise ValueError( f"real_output index {src_entr.real_output} bigger than output_keys.size() {len(src_entr.outputs)}" ) state.summary_inputs_money += src_entr.amount # Secrets derivation # the UTXO's one-time address P out_key = crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.dest) # the tx_pub of our UTXO stored inside its transaction tx_key = crypto.decodepoint(src_entr.real_out_tx_key) additional_tx_pub_key = _get_additional_public_key(src_entr) # Calculates `derivation = Ra`, private spend key `x = H(Ra||i) + b` to be able # to spend the UTXO; and key image `I = x*H(P||i)` xi, ki, _di = monero.generate_tx_spend_and_key_image_and_derivation( state.creds, state.subaddresses, out_key, tx_key, additional_tx_pub_key, src_entr.real_output_in_tx_index, state.account_idx, src_entr.subaddr_minor, ) state.mem_trace(1, True) # Construct tx.vin # If multisig is used then ki in vini should be src_entr.multisig_kLRki.ki vini = TxinToKey(amount=src_entr.amount, k_image=crypto.encodepoint(ki)) vini.key_offsets = _absolute_output_offsets_to_relative( [x.idx for x in src_entr.outputs]) if src_entr.rct: vini.amount = 0 # Serialize `vini` with variant code for TxinToKey (prefix = TxinToKey.VARIANT_CODE). # The binary `vini_bin` is later sent to step 4 and 9 with its hmac, # where it is checked and directly used. vini_bin = serialize.dump_msg(vini, preallocate=64, prefix=b"\x02") state.mem_trace(2, True) # HMAC(T_in,i || vin_i) hmac_vini = offloading_keys.gen_hmac_vini(state.key_hmac, src_entr, vini_bin, state.current_input_index) state.mem_trace(3, True) # PseudoOuts commitment, alphas stored to state alpha, pseudo_out = _gen_commitment(state, src_entr.amount) pseudo_out = crypto.encodepoint(pseudo_out) # The alpha is encrypted and passed back for storage pseudo_out_hmac = crypto.compute_hmac( offloading_keys.hmac_key_txin_comm(state.key_hmac, state.current_input_index), pseudo_out, ) alpha_enc = chacha_poly.encrypt_pack( offloading_keys.enc_key_txin_alpha(state.key_enc, state.current_input_index), crypto.encodeint(alpha), ) spend_enc = chacha_poly.encrypt_pack( offloading_keys.enc_key_spend(state.key_enc, state.current_input_index), crypto.encodeint(xi), ) state.last_step = state.STEP_INP if state.current_input_index + 1 == state.input_count: # When we finish the inputs processing, we no longer need # the precomputed subaddresses so we clear them to save memory. state.subaddresses = None state.input_last_amount = src_entr.amount return MoneroTransactionSetInputAck( vini=vini_bin, vini_hmac=hmac_vini, pseudo_out=pseudo_out, pseudo_out_hmac=pseudo_out_hmac, pseudo_out_alpha=alpha_enc, spend_key=spend_enc, )
async def sign_input( state: State, src_entr: MoneroTransactionSourceEntry, vini_bin: bytes, vini_hmac: bytes, pseudo_out: bytes, pseudo_out_hmac: bytes, pseudo_out_alpha_enc: bytes, spend_enc: bytes, ): """ :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 vini_hmac: HMAC for the tx.vin[i] as returned from Trezor :param pseudo_out: Pedersen commitment for the current input, uses pseudo_out_alpha as a mask. Only applicable for RCTTypeSimple. :param pseudo_out_hmac: HMAC for pseudo_out :param pseudo_out_alpha_enc: alpha mask used in pseudo_out, only applicable for RCTTypeSimple. Encrypted. :param spend_enc: one time address spending private key. Encrypted. :return: Generated signature MGs[i] """ from apps.monero.signing import offloading_keys await confirms.transaction_step(state.ctx, state.STEP_SIGN, state.current_input_index + 1, state.input_count) state.current_input_index += 1 if state.current_input_index >= state.input_count: raise ValueError("Invalid inputs count") if state.rct_type == RctType.Simple and pseudo_out is None: raise ValueError("SimpleRCT requires pseudo_out but none provided") if state.rct_type == RctType.Simple and pseudo_out_alpha_enc is None: raise ValueError( "SimpleRCT requires pseudo_out's mask but none provided") if state.current_input_index >= 1 and not state.rct_type == RctType.Simple: raise ValueError("Two and more inputs must imply SimpleRCT") input_position = state.source_permutation[state.current_input_index] # Check input's HMAC vini_hmac_comp = await offloading_keys.gen_hmac_vini( state.key_hmac, src_entr, vini_bin, input_position) if not crypto.ct_equals(vini_hmac_comp, vini_hmac): raise ValueError("HMAC is not correct") gc.collect() state.mem_trace(1) if state.rct_type == RctType.Simple: # both pseudo_out and its mask were offloaded so we need to # validate pseudo_out's HMAC and decrypt the alpha pseudo_out_hmac_comp = crypto.compute_hmac( offloading_keys.hmac_key_txin_comm(state.key_hmac, input_position), pseudo_out, ) if not crypto.ct_equals(pseudo_out_hmac_comp, pseudo_out_hmac): raise ValueError("HMAC is not correct") gc.collect() state.mem_trace(2) from apps.monero.xmr.crypto import chacha_poly pseudo_out_alpha = crypto.decodeint( chacha_poly.decrypt_pack( offloading_keys.enc_key_txin_alpha(state.key_enc, input_position), bytes(pseudo_out_alpha_enc), )) pseudo_out_c = crypto.decodepoint(pseudo_out) # Spending secret from apps.monero.xmr.crypto import chacha_poly from apps.monero.xmr.serialize_messages.ct_keys import CtKey spend_key = crypto.decodeint( chacha_poly.decrypt_pack( offloading_keys.enc_key_spend(state.key_enc, input_position), bytes(spend_enc), )) gc.collect() state.mem_trace(3) # Basic setup, sanity check index = src_entr.real_output input_secret_key = CtKey(dest=spend_key, mask=crypto.decodeint(src_entr.mask)) kLRki = None # for multisig: src_entr.multisig_kLRki # Private key correctness test utils.ensure( crypto.point_eq( crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.dest), crypto.scalarmult_base(input_secret_key.dest), ), "Real source entry's destination does not equal spend key's", ) utils.ensure( crypto.point_eq( crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.commitment), crypto.gen_commitment(input_secret_key.mask, src_entr.amount), ), "Real source entry's mask does not equal spend key's", ) gc.collect() state.mem_trace(4) from apps.monero.xmr import mlsag if state.rct_type == RctType.Simple: ring_pubkeys = [x.key for x in src_entr.outputs] mg = mlsag.generate_mlsag_simple( state.full_message, ring_pubkeys, input_secret_key, pseudo_out_alpha, pseudo_out_c, kLRki, index, ) else: # Full RingCt, only one input txn_fee_key = crypto.scalarmult_h(state.fee) ring_pubkeys = [[x.key] for x in src_entr.outputs] mg = mlsag.generate_mlsag_full( state.full_message, ring_pubkeys, [input_secret_key], state.output_sk_masks, state.output_pk_commitments, kLRki, index, txn_fee_key, ) gc.collect() state.mem_trace(5) # Encode mgs = _recode_msg([mg]) gc.collect() state.mem_trace(6) from trezor.messages.MoneroTransactionSignInputAck import ( MoneroTransactionSignInputAck, ) return MoneroTransactionSignInputAck( signature=serialize.dump_msg_gc(mgs[0], preallocate=488))
def prove_range_mem(amount, last_mask=None): """ Memory optimized range proof. Gives C, and mask such that \sumCi = C c.f. http:#eprint.iacr.org/2015/1098 section 5.1 Ci is a commitment to either 0 or 2^i, i=0,...,63 thus this proves that "amount" is in [0, 2^ATOMS] mask is a such that C = aG + bH, and b = amount :param amount: :param last_mask: ai[ATOMS-1] will be computed as \sum_{i=0}^{ATOMS-2} a_i - last_mask :param use_asnl: use ASNL, used before Borromean :return: sumCi, mask, RangeSig. sumCi is Pedersen commitment on the amount value. sumCi = aG + amount*H mask is "a" from the Pedersent commitment above. """ res = bytearray(32 * (64 + 64 + 64 + 1)) mv = memoryview(res) gc.collect() def as0(mv, x, i): crypto.encodeint_into(x, mv[32 * i:]) def as1(mv, x, i): crypto.encodeint_into(x, mv[32 * 64 + 32 * i:]) def aci(mv, x, i): crypto.encodepoint_into(x, mv[32 * 64 * 2 + 32 + 32 * i:]) n = 64 bb = d2b(amount, n) # gives binary form of bb in "digits" binary digits ai = key_zero_vector(n) a = crypto.sc_0() C = crypto.identity() alpha = key_zero_vector(n) c_H = crypto.gen_H() kck = crypto.get_keccak() # ee computation # First pass, generates: ai, alpha, Ci, ee, s1 for ii in range(n): ai[ii] = crypto.random_scalar() if last_mask is not None and ii == 64 - 1: ai[ii] = crypto.sc_sub(last_mask, a) a = crypto.sc_add( a, ai[ii] ) # creating the total mask since you have to pass this to receiver... alpha[ii] = crypto.random_scalar() L = crypto.scalarmult_base(alpha[ii]) if bb[ii] == 0: Ctmp = crypto.scalarmult_base(ai[ii]) else: Ctmp = crypto.point_add(crypto.scalarmult_base(ai[ii]), c_H) C = crypto.point_add(C, Ctmp) aci(mv, Ctmp, ii) if bb[ii] == 0: si = crypto.random_scalar() c = crypto.hash_to_scalar(crypto.encodepoint(L)) L = crypto.add_keys2(si, c, crypto.point_sub(Ctmp, c_H)) kck.update(crypto.encodepoint(L)) as1(mv, si, ii) else: kck.update(crypto.encodepoint(L)) c_H = crypto.point_double(c_H) # Compute ee, memory cleanup ee = crypto.sc_reduce32(crypto.decodeint(kck.digest())) crypto.encodeint_into(ee, mv[64 * 32 * 2:]) del kck gc.collect() # Second phase computes: s0, s1 c_H = crypto.gen_H() for jj in range(n): if not bb[jj]: s0 = crypto.sc_mulsub(ai[jj], ee, alpha[jj]) else: s0 = crypto.random_scalar() Ctmp = crypto.decodepoint( mv[32 * 64 * 2 + 32 + 32 * jj:32 * 64 * 2 + 32 + 32 * jj + 32]) LL = crypto.add_keys2(s0, ee, Ctmp) cc = crypto.hash_to_scalar(crypto.encodepoint(LL)) si = crypto.sc_mulsub(ai[jj], cc, alpha[jj]) as1(mv, si, jj) as0(mv, s0, jj) c_H = crypto.point_double(c_H) gc.collect() return C, a, res
async def sign_input( state: State, src_entr: MoneroTransactionSourceEntry, vini_bin: bytes, vini_hmac: bytes, pseudo_out: bytes, pseudo_out_hmac: bytes, pseudo_out_alpha_enc: bytes, spend_enc: bytes, ): """ :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 vini_hmac: HMAC for the tx.vin[i] as returned from Trezor :param pseudo_out: Pedersen commitment for the current input, uses pseudo_out_alpha as a mask. Only applicable for RCTTypeSimple. :param pseudo_out_hmac: HMAC for pseudo_out :param pseudo_out_alpha_enc: alpha mask used in pseudo_out, only applicable for RCTTypeSimple. Encrypted. :param spend_enc: one time address spending private key. Encrypted. :return: Generated signature MGs[i] """ await confirms.transaction_step(state, state.STEP_SIGN, state.current_input_index + 1) state.current_input_index += 1 if state.current_input_index >= state.input_count: raise ValueError("Invalid inputs count") if pseudo_out is None: raise ValueError("SimpleRCT requires pseudo_out but none provided") if pseudo_out_alpha_enc is None: raise ValueError( "SimpleRCT requires pseudo_out's mask but none provided") input_position = state.source_permutation[state.current_input_index] mods = utils.unimport_begin() # Check input's HMAC from apps.monero.signing import offloading_keys vini_hmac_comp = await offloading_keys.gen_hmac_vini( state.key_hmac, src_entr, vini_bin, input_position) if not crypto.ct_equals(vini_hmac_comp, vini_hmac): raise ValueError("HMAC is not correct") gc.collect() state.mem_trace(1, True) from apps.monero.xmr.crypto import chacha_poly pseudo_out_alpha = crypto.decodeint( chacha_poly.decrypt_pack( offloading_keys.enc_key_txin_alpha(state.key_enc, input_position), bytes(pseudo_out_alpha_enc), )) # Last pseud_out is recomputed so mask sums hold if state.is_det_mask() and input_position + 1 == state.input_count: # Recompute the lash alpha so the sum holds state.mem_trace("Correcting alpha") alpha_diff = crypto.sc_sub(state.sumout, state.sumpouts_alphas) crypto.sc_add_into(pseudo_out_alpha, pseudo_out_alpha, alpha_diff) pseudo_out_c = crypto.gen_commitment(pseudo_out_alpha, state.input_last_amount) else: if input_position + 1 == state.input_count: utils.ensure(crypto.sc_eq(state.sumpouts_alphas, state.sumout), "Sum eq error") # both pseudo_out and its mask were offloaded so we need to # validate pseudo_out's HMAC and decrypt the alpha pseudo_out_hmac_comp = crypto.compute_hmac( offloading_keys.hmac_key_txin_comm(state.key_hmac, input_position), pseudo_out, ) if not crypto.ct_equals(pseudo_out_hmac_comp, pseudo_out_hmac): raise ValueError("HMAC is not correct") pseudo_out_c = crypto.decodepoint(pseudo_out) state.mem_trace(2, True) # Spending secret spend_key = crypto.decodeint( chacha_poly.decrypt_pack( offloading_keys.enc_key_spend(state.key_enc, input_position), bytes(spend_enc), )) del ( offloading_keys, chacha_poly, pseudo_out, pseudo_out_hmac, pseudo_out_alpha_enc, spend_enc, ) utils.unimport_end(mods) state.mem_trace(3, True) from apps.monero.xmr.serialize_messages.ct_keys import CtKey # Basic setup, sanity check index = src_entr.real_output input_secret_key = CtKey(dest=spend_key, mask=crypto.decodeint(src_entr.mask)) kLRki = None # for multisig: src_entr.multisig_kLRki # Private key correctness test utils.ensure( crypto.point_eq( crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.dest), crypto.scalarmult_base(input_secret_key.dest), ), "Real source entry's destination does not equal spend key's", ) utils.ensure( crypto.point_eq( crypto.decodepoint( src_entr.outputs[src_entr.real_output].key.commitment), crypto.gen_commitment(input_secret_key.mask, src_entr.amount), ), "Real source entry's mask does not equal spend key's", ) state.mem_trace(4, True) from apps.monero.xmr import mlsag mg_buffer = [] ring_pubkeys = [x.key for x in src_entr.outputs] del src_entr mlsag.generate_mlsag_simple( state.full_message, ring_pubkeys, input_secret_key, pseudo_out_alpha, pseudo_out_c, kLRki, index, mg_buffer, ) del (input_secret_key, pseudo_out_alpha, mlsag, ring_pubkeys) state.mem_trace(5, True) from trezor.messages.MoneroTransactionSignInputAck import ( MoneroTransactionSignInputAck, ) return MoneroTransactionSignInputAck( signature=mg_buffer, pseudo_out=crypto.encodepoint(pseudo_out_c))
async def set_output(state: State, dst_entr, dst_entr_hmac, rsig_data): state.mem_trace(0, True) mods = utils.unimport_begin() await confirms.transaction_step(state.ctx, state.STEP_OUT, state.current_output_index + 1, state.output_count) state.mem_trace(1) state.current_output_index += 1 state.mem_trace(2, True) await _validate(state, dst_entr, dst_entr_hmac) # First output - we include the size of the container into the tx prefix hasher if state.current_output_index == 0: state.tx_prefix_hasher.uvarint(state.output_count) state.mem_trace(4, True) state.output_amounts.append(dst_entr.amount) 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, dst_entr.amount, rsig_data) utils.unimport_end(mods) state.mem_trace(6, True) # additional tx key if applicable additional_txkey_priv = _set_out_additional_keys(state, dst_entr) # derivation = a*R or r*A or s*C derivation = _set_out_derivation(state, dst_entr, additional_txkey_priv) # amount key = H_s(derivation || i) amount_key = crypto.derivation_to_scalar(derivation, state.current_output_index) # one-time destination address P = H_s(derivation || i)*G + B tx_out_key = crypto.derive_public_key( derivation, state.current_output_index, 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_out_tx_out(state, dst_entr, tx_out_key) state.mem_trace(11, True) out_pk_dest, out_pk_commitment, ecdh_info_bin = _get_ecdh_info_and_out_pk( state=state, tx_out_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. We choose to hash ECDH first, because it saves state space. state.full_message_hasher.set_ecdh(ecdh_info_bin) state.mem_trace(13, True) # output_pk_commitment is stored to the state as it is used during the signature and hashed to the # RctSigBase later. No need to store amount, it was already stored. state.output_pk_commitments.append(out_pk_commitment) 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_commitment, 0, 32) return MoneroTransactionSetOutputAck( tx_out=tx_out_bin, vouti_hmac=hmac_vouti, rsig_data=_return_rsig_data(rsig), out_pk=out_pk_bin, ecdh_info=ecdh_info_bin, )