2018-06-21 14:28:34 +00:00
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2018 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
2018-04-04 01:50:22 +00:00
import base64
import struct
import xdrlib
2018-06-28 02:26:21 +00:00
from . import messages
2018-06-13 17:04:18 +00:00
from . tools import field , expect , CallException
2018-04-19 21:10:27 +00:00
# Memo types
2018-06-27 07:19:37 +00:00
MEMO_TYPE_NONE = 0
MEMO_TYPE_TEXT = 1
MEMO_TYPE_ID = 2
MEMO_TYPE_HASH = 3
2018-04-19 21:10:27 +00:00
MEMO_TYPE_RETURN = 4
# Asset types
2018-04-28 18:59:08 +00:00
ASSET_TYPE_NATIVE = 0
ASSET_TYPE_ALPHA4 = 1
2018-04-19 21:10:27 +00:00
ASSET_TYPE_ALPHA12 = 2
# Operations
2018-04-28 18:59:08 +00:00
OP_CREATE_ACCOUNT = 0
OP_PAYMENT = 1
OP_PATH_PAYMENT = 2
OP_MANAGE_OFFER = 3
2018-04-19 21:10:27 +00:00
OP_CREATE_PASSIVE_OFFER = 4
2018-04-28 18:59:08 +00:00
OP_SET_OPTIONS = 5
OP_CHANGE_TRUST = 6
OP_ALLOW_TRUST = 7
OP_ACCOUNT_MERGE = 8
OP_INFLATION = 9 # Included for documentation purposes, not supported by Trezor
OP_MANAGE_DATA = 10
OP_BUMP_SEQUENCE = 11
2018-04-19 21:10:27 +00:00
2018-05-28 13:44:43 +00:00
DEFAULT_BIP32_PATH = " m/44h/148h/0h "
2018-06-26 09:52:47 +00:00
# Stellar's BIP32 differs to Bitcoin's see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0005.md
2018-06-13 17:04:18 +00:00
DEFAULT_NETWORK_PASSPHRASE = " Public Global Stellar Network ; September 2015 "
2018-04-04 01:50:22 +00:00
def address_from_public_key ( pk_bytes ) :
""" Returns the base32-encoded version of pk_bytes (G...)
"""
final_bytes = bytearray ( )
# version
final_bytes . append ( 6 << 3 )
# public key
final_bytes . extend ( pk_bytes )
# checksum
final_bytes . extend ( struct . pack ( " <H " , _crc16_checksum ( final_bytes ) ) )
2018-06-28 02:24:34 +00:00
return str ( base64 . b32encode ( final_bytes ) , ' utf-8 ' )
2018-04-04 01:50:22 +00:00
2018-04-28 18:59:08 +00:00
2018-04-04 01:50:22 +00:00
def address_to_public_key ( address_str ) :
""" Returns the raw 32 bytes representing a public key by extracting
it from the G . . . string
"""
decoded = base64 . b32decode ( address_str )
# skip 0th byte (version) and last two bytes (checksum)
return decoded [ 1 : - 2 ]
2018-04-20 16:34:26 +00:00
def parse_transaction_bytes ( tx_bytes ) :
2018-04-19 21:10:27 +00:00
""" Parses base64data into a map with the following keys:
tx - a StellarSignTx describing the transaction header
operations - an array of protobuf message objects for each operation
2018-04-04 01:50:22 +00:00
"""
2018-06-28 02:26:21 +00:00
tx = messages . StellarSignTx (
2018-04-19 21:10:27 +00:00
protocol_version = 1
)
2018-04-20 16:34:26 +00:00
unpacker = xdrlib . Unpacker ( tx_bytes )
2018-04-04 01:50:22 +00:00
2018-04-19 21:10:27 +00:00
tx . source_account = _xdr_read_address ( unpacker )
tx . fee = unpacker . unpack_uint ( )
tx . sequence_number = unpacker . unpack_uhyper ( )
2018-04-04 01:50:22 +00:00
# Timebounds is an optional field
2018-04-28 18:39:55 +00:00
if unpacker . unpack_bool ( ) :
2018-04-28 18:59:08 +00:00
max_timebound = 2 * * 32 - 1 # max unsigned 32-bit int (trezor does not support the full 64-bit time value)
2018-04-19 21:10:27 +00:00
tx . timebounds_start = unpacker . unpack_uhyper ( )
tx . timebounds_end = unpacker . unpack_uhyper ( )
2018-04-04 01:50:22 +00:00
2018-04-19 21:10:27 +00:00
if tx . timebounds_start > max_timebound or tx . timebounds_start < 0 :
2018-04-04 01:50:22 +00:00
raise ValueError ( " Starting timebound out of range (must be between 0 and " + max_timebound )
2018-04-19 21:10:27 +00:00
if tx . timebounds_end > max_timebound or tx . timebounds_end < 0 :
2018-04-04 01:50:22 +00:00
raise ValueError ( " Ending timebound out of range (must be between 0 and " + max_timebound )
# memo type determines what optional fields are set
2018-04-19 21:10:27 +00:00
tx . memo_type = unpacker . unpack_uint ( )
2018-04-04 01:50:22 +00:00
# text
2018-06-27 07:19:37 +00:00
if tx . memo_type == MEMO_TYPE_TEXT :
2018-04-19 21:10:27 +00:00
tx . memo_text = unpacker . unpack_string ( )
2018-04-04 01:50:22 +00:00
# id (64-bit uint)
2018-04-19 21:10:27 +00:00
if tx . memo_type == MEMO_TYPE_ID :
tx . memo_id = unpacker . unpack_uhyper ( )
2018-04-04 01:50:22 +00:00
# hash / return are the same structure (32 bytes representing a hash)
2018-04-19 21:10:27 +00:00
if tx . memo_type == MEMO_TYPE_HASH or tx . memo_type == MEMO_TYPE_RETURN :
tx . memo_hash = unpacker . unpack_fopaque ( 32 )
2018-04-04 01:50:22 +00:00
2018-04-19 21:10:27 +00:00
tx . num_operations = unpacker . unpack_uint ( )
2018-04-04 01:50:22 +00:00
2018-04-28 18:39:55 +00:00
operations = [ ]
2018-04-19 21:10:27 +00:00
for i in range ( tx . num_operations ) :
operations . append ( _parse_operation_bytes ( unpacker ) )
2018-04-04 01:50:22 +00:00
2018-04-20 16:34:26 +00:00
return tx , operations
2018-04-04 01:50:22 +00:00
2018-04-28 18:59:08 +00:00
2018-04-04 01:50:22 +00:00
def _parse_operation_bytes ( unpacker ) :
2018-04-19 21:10:27 +00:00
""" Returns a protobuf message representing the next operation as read from
2018-04-04 01:50:22 +00:00
the byte stream in unpacker
"""
2018-04-19 21:10:27 +00:00
# Check for and parse optional source account field
source_account = None
2018-04-28 18:39:55 +00:00
if unpacker . unpack_bool ( ) :
2018-04-19 21:10:27 +00:00
source_account = unpacker . unpack_fopaque ( 32 )
# Operation type (See OP_ constants)
type = unpacker . unpack_uint ( )
if type == OP_CREATE_ACCOUNT :
2018-06-28 02:26:21 +00:00
return messages . StellarCreateAccountOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
new_account = _xdr_read_address ( unpacker ) ,
starting_balance = unpacker . unpack_hyper ( )
)
if type == OP_PAYMENT :
2018-06-28 02:26:21 +00:00
return messages . StellarPaymentOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
destination_account = _xdr_read_address ( unpacker ) ,
asset = _xdr_read_asset ( unpacker ) ,
amount = unpacker . unpack_hyper ( )
)
if type == OP_PATH_PAYMENT :
2018-06-28 02:26:21 +00:00
op = messages . StellarPathPaymentOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
send_asset = _xdr_read_asset ( unpacker ) ,
send_max = unpacker . unpack_hyper ( ) ,
destination_account = _xdr_read_address ( unpacker ) ,
destination_asset = _xdr_read_asset ( unpacker ) ,
2018-06-27 07:20:38 +00:00
destination_amount = unpacker . unpack_hyper ( ) ,
2018-04-19 21:10:27 +00:00
paths = [ ]
)
2018-04-04 01:50:22 +00:00
num_paths = unpacker . unpack_uint ( )
2018-04-19 21:10:27 +00:00
for i in range ( num_paths ) :
op . paths . append ( _xdr_read_asset ( unpacker ) )
return op
if type == OP_MANAGE_OFFER :
2018-06-28 02:26:21 +00:00
return messages . StellarManageOfferOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
selling_asset = _xdr_read_asset ( unpacker ) ,
buying_asset = _xdr_read_asset ( unpacker ) ,
amount = unpacker . unpack_hyper ( ) ,
price_n = unpacker . unpack_uint ( ) ,
price_d = unpacker . unpack_uint ( ) ,
offer_id = unpacker . unpack_uhyper ( )
)
if type == OP_CREATE_PASSIVE_OFFER :
2018-06-28 02:26:21 +00:00
return messages . StellarCreatePassiveOfferOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
selling_asset = _xdr_read_asset ( unpacker ) ,
buying_asset = _xdr_read_asset ( unpacker ) ,
amount = unpacker . unpack_hyper ( ) ,
price_n = unpacker . unpack_uint ( ) ,
price_d = unpacker . unpack_uint ( )
)
if type == OP_SET_OPTIONS :
2018-06-28 02:26:21 +00:00
op = messages . StellarSetOptionsOp (
2018-04-19 21:10:27 +00:00
source_account = source_account
)
# Inflation destination
if unpacker . unpack_bool ( ) :
op . inflation_destination_account = _xdr_read_address ( unpacker )
# clear flags
if unpacker . unpack_bool ( ) :
op . clear_flags = unpacker . unpack_uint ( )
# set flags
if unpacker . unpack_bool ( ) :
op . set_flags = unpacker . unpack_uint ( )
# master weight
if unpacker . unpack_bool ( ) :
op . master_weight = unpacker . unpack_uint ( )
# low threshold
if unpacker . unpack_bool ( ) :
op . low_threshold = unpacker . unpack_uint ( )
# medium threshold
if unpacker . unpack_bool ( ) :
op . medium_threshold = unpacker . unpack_uint ( )
# high threshold
if unpacker . unpack_bool ( ) :
op . high_threshold = unpacker . unpack_uint ( )
# home domain
if unpacker . unpack_bool ( ) :
op . home_domain = unpacker . unpack_string ( )
# signer
if unpacker . unpack_bool ( ) :
op . signer_type = unpacker . unpack_uint ( )
op . signer_key = unpacker . unpack_fopaque ( 32 )
op . signer_weight = unpacker . unpack_uint ( )
return op
if type == OP_CHANGE_TRUST :
2018-06-28 02:26:21 +00:00
return messages . StellarChangeTrustOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
asset = _xdr_read_asset ( unpacker ) ,
limit = unpacker . unpack_uhyper ( )
)
if type == OP_ALLOW_TRUST :
2018-06-28 02:26:21 +00:00
op = messages . StellarAllowTrustOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
trusted_account = _xdr_read_address ( unpacker ) ,
asset_type = unpacker . unpack_uint ( )
)
if op . asset_type == ASSET_TYPE_ALPHA4 :
op . asset_code = unpacker . unpack_fstring ( 4 )
if op . asset_type == ASSET_TYPE_ALPHA12 :
op . asset_code = unpacker . unpack_fstring ( 12 )
op . is_authorized = unpacker . unpack_bool ( )
return op
if type == OP_ACCOUNT_MERGE :
2018-06-28 02:26:21 +00:00
return messages . StellarAccountMergeOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
destination_account = _xdr_read_address ( unpacker )
)
# Inflation is not implemented since anyone can submit this operation to the network
if type == OP_MANAGE_DATA :
2018-06-28 02:26:21 +00:00
op = messages . StellarManageDataOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
key = unpacker . unpack_string ( ) ,
)
# Only set value if the field is present
if unpacker . unpack_bool ( ) :
op . value = unpacker . unpack_opaque ( )
return op
2018-04-04 01:50:22 +00:00
# Bump Sequence
# see: https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-transaction.x#L269
2018-04-19 21:10:27 +00:00
if type == OP_BUMP_SEQUENCE :
2018-06-28 02:26:21 +00:00
return messages . StellarBumpSequenceOp (
2018-04-19 21:10:27 +00:00
source_account = source_account ,
bump_to = unpacker . unpack_uhyper ( )
)
2018-04-04 01:50:22 +00:00
2018-06-27 07:20:05 +00:00
raise ValueError ( " Unknown operation type: " + str ( type ) )
2018-04-04 01:50:22 +00:00
2018-04-28 18:59:08 +00:00
2018-04-04 01:50:22 +00:00
def _xdr_read_asset ( unpacker ) :
""" Reads a stellar Asset from unpacker """
2018-06-28 02:26:21 +00:00
asset = messages . StellarAssetType (
2018-04-19 21:10:27 +00:00
type = unpacker . unpack_uint ( )
)
2018-04-04 01:50:22 +00:00
2018-04-19 21:10:27 +00:00
if asset . type == ASSET_TYPE_ALPHA4 :
asset . code = unpacker . unpack_fstring ( 4 )
asset . issuer = _xdr_read_address ( unpacker )
2018-04-04 01:50:22 +00:00
2018-04-19 21:10:27 +00:00
if asset . type == ASSET_TYPE_ALPHA12 :
asset . code = unpacker . unpack_fstring ( 12 )
asset . issuer = _xdr_read_address ( unpacker )
2018-04-04 01:50:22 +00:00
return asset
def _xdr_read_address ( unpacker ) :
2018-06-24 02:54:58 +00:00
""" Reads a stellar address and returns the string representing the address
This method assumes the encoded address is a public address ( starting with G )
2018-04-04 01:50:22 +00:00
"""
# First 4 bytes are the address type
address_type = unpacker . unpack_uint ( )
if address_type != 0 :
raise ValueError ( " Unsupported address type " )
2018-06-24 02:54:58 +00:00
return address_from_public_key ( unpacker . unpack_fopaque ( 32 ) )
2018-04-04 01:50:22 +00:00
2018-04-28 18:59:08 +00:00
2018-04-04 01:50:22 +00:00
def _crc16_checksum ( bytes ) :
""" Returns the CRC-16 checksum of bytearray bytes
Ported from Java implementation at : http : / / introcs . cs . princeton . edu / java / 61 data / CRC16CCITT . java . html
Initial value changed to 0x0000 to match Stellar configuration .
"""
crc = 0x0000
polynomial = 0x1021
for byte in bytes :
2018-04-28 18:39:55 +00:00
for i in range ( 8 ) :
2018-04-04 01:50:22 +00:00
bit = ( ( byte >> ( 7 - i ) & 1 ) == 1 )
c15 = ( ( crc >> 15 & 1 ) == 1 )
crc << = 1
if c15 ^ bit :
crc ^ = polynomial
2018-04-28 18:39:55 +00:00
return crc & 0xffff
2018-06-13 17:04:18 +00:00
### Client functions ###
@field ( ' public_key ' )
@expect ( messages . StellarPublicKey )
def get_public_key ( client , address_n , show_display = False ) :
return client . call ( messages . StellarGetPublicKey ( address_n = address_n , show_display = show_display ) )
@field ( ' address ' )
@expect ( messages . StellarAddress )
def get_address ( client , address_n , show_display = False ) :
return client . call ( messages . StellarGetAddress ( address_n = address_n , show_display = show_display ) )
def sign_tx ( client , tx , operations , address_n , network_passphrase = DEFAULT_NETWORK_PASSPHRASE ) :
tx . network_passphrase = network_passphrase
tx . address_n = address_n
tx . num_operations = len ( operations )
# Signing loop works as follows:
#
# 1. Start with tx (header information for the transaction) and operations (an array of operation protobuf messagess)
# 2. Send the tx header to the device
# 3. Receive a StellarTxOpRequest message
# 4. Send operations one by one until all operations have been sent. If there are more operations to sign, the device will send a StellarTxOpRequest message
# 5. The final message received will be StellarSignedTx which is returned from this method
resp = client . call ( tx )
try :
while isinstance ( resp , messages . StellarTxOpRequest ) :
resp = client . call ( operations . pop ( 0 ) )
except IndexError :
# pop from empty list
raise CallException ( " Stellar.UnexpectedEndOfOperations " ,
" Reached end of operations without a signature. " ) from None
if not isinstance ( resp , messages . StellarSignedTx ) :
raise CallException ( messages . FailureType . UnexpectedMessage , resp )
if operations :
raise CallException ( " Stellar.UnprocessedOperations " ,
" Received a signature before processing all operations. " )
return resp