You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/apps/monero/xmr/monero.py

317 lines
11 KiB

from typing import TYPE_CHECKING
from apps.monero.xmr import crypto, crypto_helpers
if TYPE_CHECKING:
from apps.monero.xmr.credentials import AccountCreds
Subaddresses = dict[bytes, tuple[int, int]]
class XmrException(Exception):
pass
class XmrNoSuchAddressException(XmrException):
pass
def get_subaddress_secret_key(
secret_key: crypto.Scalar, major: int = 0, minor: int = 0
) -> crypto.Scalar:
"""
Builds subaddress secret key from the subaddress index
Hs(SubAddr || a || index_major || index_minor)
"""
if major == 0 and minor == 0:
return secret_key
return crypto_helpers.get_subaddress_secret_key(secret_key, major, minor)
def get_subaddress_spend_public_key(
view_private: crypto.Scalar, spend_public: crypto.Point, major: int, minor: int
) -> crypto.Point:
"""
Generates subaddress spend public key D_{major, minor}
"""
if major == 0 and minor == 0:
return spend_public
m = get_subaddress_secret_key(view_private, major=major, minor=minor)
M = crypto.scalarmult_base_into(None, m)
D = crypto.point_add_into(None, spend_public, M)
return D
def derive_subaddress_public_key(
out_key: crypto.Point, derivation: crypto.Point, output_index: int
) -> crypto.Point:
"""
out_key - H_s(derivation || varint(output_index))G
"""
crypto.ge25519_check(out_key)
scalar = crypto_helpers.derivation_to_scalar(derivation, output_index)
point2 = crypto.scalarmult_base_into(None, scalar)
point4 = crypto.point_sub_into(None, out_key, point2)
return point4
def generate_key_image(public_key: bytes, secret_key: crypto.Scalar) -> crypto.Point:
"""
Key image: secret_key * H_p(pub_key)
"""
point = crypto.hash_to_point_into(None, public_key)
point2 = crypto.scalarmult_into(None, point, secret_key)
return point2
def is_out_to_account(
subaddresses: Subaddresses,
out_key: crypto.Point,
derivation: crypto.Point,
additional_derivation: crypto.Point | None,
output_index: int,
creds: AccountCreds | None,
sub_addr_major: int | None,
sub_addr_minor: int | None,
) -> tuple[tuple[int, int], crypto.Point] | None:
"""
Checks whether the given transaction is sent to the account.
Searches subaddresses for the computed subaddress_spendkey.
Corresponds to is_out_to_acc_precomp() in the Monero codebase.
If found, returns (major, minor), derivation, otherwise None.
If (creds, sub_addr_major, sub_addr_minor) are specified,
subaddress is checked directly (avoids the need to store
large subaddresses dicts).
"""
subaddress_spendkey_obj = derive_subaddress_public_key(
out_key, derivation, output_index
)
sub_pub_key = None
if creds and sub_addr_major is not None and sub_addr_minor is not None:
sub_pub_key = get_subaddress_spend_public_key(
creds.view_key_private,
creds.spend_key_public,
sub_addr_major,
sub_addr_minor,
)
if crypto.point_eq(subaddress_spendkey_obj, sub_pub_key):
return (sub_addr_major, sub_addr_minor), derivation
if subaddresses:
subaddress_spendkey = crypto_helpers.encodepoint(subaddress_spendkey_obj)
if subaddress_spendkey in subaddresses:
return subaddresses[subaddress_spendkey], derivation
if additional_derivation:
subaddress_spendkey_obj = derive_subaddress_public_key(
out_key, additional_derivation, output_index
)
if sub_pub_key and crypto.point_eq(subaddress_spendkey_obj, sub_pub_key):
# sub_pub_key is only set if sub_addr_{major, minor} are set
assert sub_addr_major is not None and sub_addr_minor is not None
return (sub_addr_major, sub_addr_minor), additional_derivation
if subaddresses:
subaddress_spendkey = crypto_helpers.encodepoint(subaddress_spendkey_obj)
if subaddress_spendkey in subaddresses:
return subaddresses[subaddress_spendkey], additional_derivation
return None
def generate_tx_spend_and_key_image(
ack: AccountCreds,
out_key: crypto.Point,
recv_derivation: crypto.Point,
real_output_index: int,
received_index: tuple[int, int],
) -> tuple[crypto.Scalar, crypto.Point]:
"""
Generates UTXO spending key and key image.
Corresponds to generate_key_image_helper_precomp() in the Monero codebase.
:param ack: sender credentials
:type ack: apps.monero.xmr.credentials.AccountCreds
:param out_key: real output (from input RCT) destination key
:param recv_derivation:
:param real_output_index:
:param received_index: subaddress index this payment was received to
:return:
"""
if crypto.sc_iszero(ack.spend_key_private):
raise ValueError("Watch-only wallet not supported")
# derive secret key with subaddress - step 1: original CN derivation
scalar_step1 = crypto_helpers.derive_secret_key(
recv_derivation, real_output_index, ack.spend_key_private
)
# step 2: add Hs(SubAddr || a || index_major || index_minor)
subaddr_sk = None
if received_index == (0, 0):
scalar_step2 = scalar_step1
else:
subaddr_sk = get_subaddress_secret_key(
ack.view_key_private, major=received_index[0], minor=received_index[1]
)
scalar_step2 = crypto.sc_add_into(None, scalar_step1, subaddr_sk)
# When not in multisig, we know the full spend secret key, so the output pubkey can be obtained by scalarmultBase
pub_ver = crypto.scalarmult_base_into(None, scalar_step2)
# <Multisig>, branch deactivated until implemented
# # When in multisig, we only know the partial spend secret key. But we do know the full spend public key,
# # so the output pubkey can be obtained by using the standard CN key derivation.
# pub_ver = crypto.derive_public_key(
# recv_derivation, real_output_index, ack.spend_key_public
# )
#
# # Add the contribution from the subaddress part
# if received_index != (0, 0):
# subaddr_pk = crypto.scalarmult_base(subaddr_sk)
# pub_ver = crypto.point_add(pub_ver, subaddr_pk)
# </Multisig>
if not crypto.point_eq(pub_ver, out_key):
raise ValueError(
"key image helper precomp: given output pubkey doesn't match the derived one"
)
ki = generate_key_image(crypto_helpers.encodepoint(pub_ver), scalar_step2)
return scalar_step2, ki
def generate_tx_spend_and_key_image_and_derivation(
creds: AccountCreds,
subaddresses: Subaddresses,
out_key: crypto.Point,
tx_public_key: crypto.Point,
additional_tx_public_key: crypto.Point | None,
real_output_index: int | None,
sub_addr_major: int | None,
sub_addr_minor: int | None,
) -> tuple[crypto.Scalar, crypto.Point, crypto.Point]:
"""
Generates UTXO spending key and key image and corresponding derivation.
Supports subaddresses.
Corresponds to generate_key_image_helper() in the Monero codebase.
:param creds:
:param subaddresses:
:param out_key: real output (from input RCT) destination key
:param tx_public_key: R, transaction public key
:param additional_tx_public_key: Additional Rs, for subaddress destinations
:param real_output_index: index of the real output in the RCT
:param sub_addr_major: subaddress major index
:param sub_addr_minor: subaddress minor index
:return:
"""
recv_derivation = crypto_helpers.generate_key_derivation(
tx_public_key, creds.view_key_private
)
additional_recv_derivation = (
crypto_helpers.generate_key_derivation(
additional_tx_public_key, creds.view_key_private
)
if additional_tx_public_key
else None
)
subaddr_recv_info = is_out_to_account(
subaddresses,
out_key,
recv_derivation,
additional_recv_derivation,
real_output_index,
creds,
sub_addr_major,
sub_addr_minor,
)
if subaddr_recv_info is None:
raise XmrNoSuchAddressException("No such addr")
xi, ki = generate_tx_spend_and_key_image(
creds, out_key, subaddr_recv_info[1], real_output_index, subaddr_recv_info[0]
)
return xi, ki, recv_derivation
def compute_subaddresses(
creds: AccountCreds,
account: int,
indices: list[int],
subaddresses: Subaddresses | None = None,
) -> Subaddresses:
"""
Computes subaddress public spend key for receiving transactions.
:param creds: credentials
:param account: major index
:param indices: array of minor indices
:param subaddresses: subaddress dict. optional.
:return:
"""
if subaddresses is None:
subaddresses = {}
for idx in indices:
if account == 0 and idx == 0:
subaddresses[crypto_helpers.encodepoint(creds.spend_key_public)] = (0, 0)
continue
pub = get_subaddress_spend_public_key(
creds.view_key_private, creds.spend_key_public, major=account, minor=idx
)
pub = crypto_helpers.encodepoint(pub)
subaddresses[pub] = (account, idx)
return subaddresses
def generate_keys(recovery_key: crypto.Scalar) -> tuple[crypto.Scalar, crypto.Point]:
pub = crypto.scalarmult_base_into(None, recovery_key)
return recovery_key, pub
def generate_monero_keys(
seed: bytes,
) -> tuple[crypto.Scalar, crypto.Point, crypto.Scalar, crypto.Point]:
"""
Generates spend key / view key from the seed in the same manner as Monero code does.
account.cpp:
crypto::secret_key account_base::generate(const crypto::secret_key& recovery_key, bool recover, bool two_random).
"""
spend_sec, spend_pub = generate_keys(crypto_helpers.decodeint(seed))
hash = crypto.fast_hash_into(None, crypto_helpers.encodeint(spend_sec))
view_sec, view_pub = generate_keys(crypto_helpers.decodeint(hash))
return spend_sec, spend_pub, view_sec, view_pub
def generate_sub_address_keys(
view_sec: crypto.Scalar, spend_pub: crypto.Point, major: int, minor: int
) -> tuple[crypto.Point, crypto.Point]:
if major == 0 and minor == 0: # special case, Monero-defined
return spend_pub, crypto.scalarmult_base_into(None, view_sec)
m = get_subaddress_secret_key(view_sec, major=major, minor=minor)
M = crypto.scalarmult_base_into(None, m)
D = crypto.point_add_into(None, spend_pub, M)
C = crypto.scalarmult_into(None, D, view_sec)
return D, C
def commitment_mask(key: bytes, buff: crypto.Scalar | None = None) -> crypto.Scalar:
"""
Generates deterministic commitment mask for Bulletproof2
"""
data = bytearray(15 + 32)
data[0:15] = b"commitment_mask"
data[15:] = key
return crypto.hash_to_scalar_into(buff, data)