diff --git a/src/apps/stellar/helpers.py b/src/apps/stellar/helpers.py index 8b74ca1352..52cb1f9460 100644 --- a/src/apps/stellar/helpers.py +++ b/src/apps/stellar/helpers.py @@ -1,21 +1,36 @@ from trezor.crypto import base32 +from trezor.wire import ProcessError import ustruct STELLAR_CURVE = 'ed25519' +def public_key_from_address(address: str) -> bytes: + """Extracts public key from an address + Stellar address is in format: + <1-byte version> <32-bytes ed25519 public key> <2-bytes CRC-16 checksum> + """ + b = base32.decode(address) + _crc16_checksum_verify(b[:-2], b[-2:]) + return b[1:-2] + + def address_from_public_key(pubkey: bytes): """Returns the base32-encoded version of public key bytes (G...)""" - address = bytearray() address.append(6 << 3) # version -> 'G' address.extend(pubkey) - address.extend(ustruct.pack(" bytes: """Returns the CRC-16 checksum of bytearray bytes Ported from Java implementation at: http://introcs.cs.princeton.edu/java/61data/CRC16CCITT.java.html @@ -33,4 +48,4 @@ def _crc16_checksum(data: bytearray): if c15 ^ bit: crc ^= polynomial - return crc & 0xffff + return ustruct.pack(" str: return utils.format_amount(amount, consts.AMOUNT_DIVISIBILITY) + t -def format_address(pubkey: bytes) -> str: - return helpers.address_from_public_key(pubkey) - - # todo merge with nem def split(text): return utils.chunks(text, 17) diff --git a/src/apps/stellar/operations/layout.py b/src/apps/stellar/operations/layout.py index 1a292dd337..110764e4cc 100644 --- a/src/apps/stellar/operations/layout.py +++ b/src/apps/stellar/operations/layout.py @@ -1,4 +1,4 @@ -from apps.stellar.layout import split, format_address, format_amount, ui, trim_to_rows, require_confirm +from apps.stellar.layout import split, format_amount, ui, trim_to_rows, require_confirm from apps.stellar import consts from trezor.messages import ButtonRequestType from trezor.ui.text import Text @@ -20,7 +20,7 @@ from ubinascii import hexlify async def confirm_source_account(ctx, source_account: bytes): content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, 'Source account:', - ui.MONO, *split(format_address(source_account)), + ui.MONO, *split(source_account), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) @@ -33,7 +33,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp): content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, text, ui.NORMAL, 'of %s by:' % op.asset_code, - ui.MONO, *split(trim_to_rows(format_address(op.trusted_account), 3)), + ui.MONO, *split(trim_to_rows(op.trusted_account, 3)), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) @@ -43,7 +43,7 @@ async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp): content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, 'Account Merge', ui.NORMAL, 'All XLM will be sent to:', - ui.MONO, *split(trim_to_rows(format_address(op.destination_account), 3)), + ui.MONO, *split(trim_to_rows(op.destination_account, 3)), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) @@ -75,7 +75,7 @@ async def confirm_create_account_op(ctx, op: StellarCreateAccountOp): content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, 'Create Account', ui.NORMAL, 'with %s' % format_amount(op.starting_balance), - ui.MONO, *split(trim_to_rows(format_address(op.new_account), 3)), + ui.MONO, *split(trim_to_rows(op.new_account, 3)), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) @@ -137,7 +137,7 @@ async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp): content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, 'Path Pay %s' % format_amount(op.destination_amount, ticker=False), ui.BOLD, '%s to:' % format_asset_code(op.destination_asset), - ui.MONO, *split(trim_to_rows(format_address(op.destination_account), 3)), + ui.MONO, *split(trim_to_rows(op.destination_account, 3)), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) await confirm_asset_issuer(ctx, op.destination_asset) @@ -157,7 +157,7 @@ async def confirm_payment_op(ctx, op: StellarPaymentOp): content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, 'Pay %s' % format_amount(op.amount, ticker=False), ui.BOLD, '%s to:' % format_asset_code(op.asset), - ui.MONO, *split(trim_to_rows(format_address(op.destination_account), 3)), + ui.MONO, *split(trim_to_rows(op.destination_account, 3)), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) await confirm_asset_issuer(ctx, op.asset) @@ -167,7 +167,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp): if op.inflation_destination_account: content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, 'Set Inflation Destination', - ui.MONO, *split(format_address(op.inflation_destination_account)), + ui.MONO, *split(op.inflation_destination_account), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) if op.clear_flags: @@ -205,7 +205,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp): if op.signer_type == consts.SIGN_TYPE_ACCOUNT: content = Text('Confirm operation', ui.ICON_CONFIRM, ui.BOLD, text % 'acc', - ui.MONO, *split(format_address(op.signer_key)), + ui.MONO, *split(op.signer_key), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) elif op.signer_type in (consts.SIGN_TYPE_PRE_AUTH, consts.SIGN_TYPE_HASH): @@ -259,6 +259,6 @@ async def confirm_asset_issuer(ctx, asset: StellarAssetType): return content = Text('Confirm issuer', ui.ICON_CONFIRM, ui.BOLD, '%s issuer:' % asset.code, - ui.MONO, *split(format_address(asset.issuer)), + ui.MONO, *split(asset.issuer), icon_color=ui.GREEN) await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) diff --git a/src/apps/stellar/operations/serialize.py b/src/apps/stellar/operations/serialize.py index 248dd1bf0d..ea664c8201 100644 --- a/src/apps/stellar/operations/serialize.py +++ b/src/apps/stellar/operations/serialize.py @@ -135,7 +135,7 @@ def serialize_set_options_op(w, msg: StellarSetOptionsOp): writers.write_uint32(w, msg.signer_weight) -def serialize_account(w, source_account: bytes): +def serialize_account(w, source_account: str): if source_account is None: writers.write_bool(w, False) return diff --git a/src/apps/stellar/sign_tx.py b/src/apps/stellar/sign_tx.py index e1503ecd04..774da77f5c 100644 --- a/src/apps/stellar/sign_tx.py +++ b/src/apps/stellar/sign_tx.py @@ -1,8 +1,10 @@ from apps.common import seed from apps.stellar import writers from apps.stellar.operations import operation +from apps.stellar import helpers from apps.stellar import layout from apps.stellar import consts +from trezor.wire import ProcessError from trezor.messages.StellarSignTx import StellarSignTx from trezor.messages.StellarTxOpRequest import StellarTxOpRequest from trezor.messages.StellarSignedTx import StellarSignedTx @@ -45,14 +47,15 @@ async def _init(ctx, w: bytearray, pubkey: bytes, msg: StellarSignTx): writers.write_bytes(w, network_passphrase_hash) writers.write_bytes(w, consts.TX_TYPE) - writers.write_pubkey(w, pubkey) - if msg.source_account != pubkey: - raise ValueError('Stellar: source account does not match address_n') + address = helpers.address_from_public_key(pubkey) + writers.write_pubkey(w, address) + if helpers.public_key_from_address(msg.source_account) != pubkey: + raise ProcessError('Stellar: source account does not match address_n') writers.write_uint32(w, msg.fee) writers.write_uint64(w, msg.sequence_number) # confirm init - await layout.require_confirm_init(ctx, pubkey, msg.network_passphrase) + await layout.require_confirm_init(ctx, address, msg.network_passphrase) def _timebounds(w: bytearray, start: int, end: int): diff --git a/src/apps/stellar/writers.py b/src/apps/stellar/writers.py index 606f742b07..056cd19d63 100644 --- a/src/apps/stellar/writers.py +++ b/src/apps/stellar/writers.py @@ -1,4 +1,5 @@ import ustruct +from .helpers import public_key_from_address def write_uint32(w, n: int): @@ -29,7 +30,8 @@ def write_bool(w, val: True): write_uint32(w, 0) -def write_pubkey(w, pubkey: bytes): +def write_pubkey(w, address: str): # first 4 bytes of an address are the type, there's only one type (0) write_uint32(w, 0) + pubkey = public_key_from_address(address) write_bytes(w, bytearray(pubkey)) diff --git a/tests/test_apps.stellar.address_to_pubkey.py b/tests/test_apps.stellar.address_to_pubkey.py new file mode 100644 index 0000000000..2beb1c4e8f --- /dev/null +++ b/tests/test_apps.stellar.address_to_pubkey.py @@ -0,0 +1,38 @@ +from common import * +from apps.stellar.helpers import address_from_public_key, public_key_from_address +from trezor.wire import ProcessError + + +class TestStellarAddressToPubkey(unittest.TestCase): + + def test_address_to_pubkey(self): + self.assertEqual(public_key_from_address('GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V'), + unhexlify('5d55642466b185b843152e9e219151dbc5892027ec40101a517bed5ca030c2e0')) + + self.assertEqual(public_key_from_address('GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2U6'), + unhexlify('9ba568e6eec16bea4fed0f17b134a1d692eae199a578ba6fb44c0c24431bfdb4')) + + def test_pubkey_to_address(self): + addr = address_from_public_key(unhexlify('5d55642466b185b843152e9e219151dbc5892027ec40101a517bed5ca030c2e0')) + self.assertEqual(addr, 'GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V') + + addr = address_from_public_key(unhexlify('9ba568e6eec16bea4fed0f17b134a1d692eae199a578ba6fb44c0c24431bfdb4')) + self.assertEqual(addr, 'GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2U6') + + def test_both(self): + pubkey = unhexlify('dfcc77d08588601702e02de2dc603f5c5281bea23baa894ae3b3b4778e5bbe40') + self.assertEqual(public_key_from_address(address_from_public_key(pubkey)), pubkey) + + pubkey = unhexlify('53214e6155469c32fb882b1b1d94930d5445a78202867b7ddc6a33ad42ff4464') + self.assertEqual(public_key_from_address(address_from_public_key(pubkey)), pubkey) + + pubkey = unhexlify('5ed4690134e5ef79b290ea1e7a4b8f3b6b3bcf287463c18bfe36baa030e7efbd') + self.assertEqual(public_key_from_address(address_from_public_key(pubkey)), pubkey) + + def test_invalid_address(self): + with self.assertRaises(ProcessError): + public_key_from_address('GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2AA') # invalid checksum + + +if __name__ == '__main__': + unittest.main()