python-trezor/trezorlib/client.py

823 lines
29 KiB
Python
Raw Normal View History

import os
import sys
2014-04-09 12:42:30 -07:00
import time
2014-01-05 16:54:53 -08:00
import binascii
2014-01-08 06:59:18 -08:00
import hashlib
import unicodedata
import mapping
import json
import getpass
import tools
2013-12-16 07:26:40 -08:00
import messages_pb2 as proto
import types_pb2 as types
import protobuf_json
2014-02-13 07:46:21 -08:00
from trezorlib.debuglink import DebugLink
2014-02-20 16:48:11 -08:00
from mnemonic import Mnemonic
# try:
# from PIL import Image
# SCREENSHOT = True
# except:
# SCREENSHOT = False
SCREENSHOT = False
DEFAULT_CURVE = 'secp256k1'
# monkeypatching: text formatting of protobuf messages
tools.monkeypatch_google_protobuf_text_format()
2014-02-13 07:46:21 -08:00
def get_buttonrequest_value(code):
# Converts integer code to its string representation of ButtonRequestType
return [ k for k, v in types.ButtonRequestType.items() if v == code][0]
2014-02-02 09:27:44 -08:00
def pprint(msg):
msg_class = msg.__class__.__name__
msg_size = msg.ByteSize()
"""
msg_ser = msg.SerializeToString()
msg_id = mapping.get_type(msg)
msg_json = json.dumps(protobuf_json.pb2json(msg))
"""
2014-02-21 10:13:14 -08:00
if isinstance(msg, proto.FirmwareUpload):
return "<%s> (%d bytes):\n" % (msg_class, msg_size)
2014-02-21 10:13:14 -08:00
else:
return "<%s> (%d bytes):\n%s" % (msg_class, msg_size, msg)
def log(msg):
sys.stderr.write("%s\n" % msg)
sys.stderr.flush()
class CallException(Exception):
def __init__(self, code, message):
super(CallException, self).__init__()
self.args = [code, message]
class PinException(CallException):
pass
2014-02-13 07:46:21 -08:00
class field(object):
# Decorator extracts single value from
# protobuf object. If the field is not
# present, raises an exception.
def __init__(self, field):
self.field = field
def __call__(self, f):
def wrapped_f(*args, **kwargs):
ret = f(*args, **kwargs)
ret.HasField(self.field)
return getattr(ret, self.field)
return wrapped_f
class expect(object):
# Decorator checks if the method
# returned one of expected protobuf messages
# or raises an exception
def __init__(self, *expected):
self.expected = expected
2016-01-12 15:17:38 -08:00
2014-02-13 07:46:21 -08:00
def __call__(self, f):
def wrapped_f(*args, **kwargs):
ret = f(*args, **kwargs)
if not isinstance(ret, self.expected):
raise Exception("Got %s, expected %s" % (ret.__class__, self.expected))
return ret
return wrapped_f
def normalize_nfc(txt):
# Normalize string to UTF8 NFC for sign_message
if isinstance(txt, str):
utxt = txt.decode('utf8')
elif isinstance(txt, unicode):
utxt = txt
else:
raise Exception("String value expected")
return unicodedata.normalize('NFC', utxt)
2014-02-13 07:46:21 -08:00
class BaseClient(object):
# Implements very basic layer of sending raw protobuf
# messages to device and getting its response back.
def __init__(self, transport, **kwargs):
2014-02-13 07:46:21 -08:00
self.transport = transport
super(BaseClient, self).__init__() # *args, **kwargs)
def call_raw(self, msg):
2014-02-13 07:46:21 -08:00
try:
self.transport.session_begin()
self.transport.write(msg)
resp = self.transport.read_blocking()
finally:
self.transport.session_end()
return resp
def call(self, msg):
try:
self.transport.session_begin()
resp = self.call_raw(msg)
2014-02-13 07:46:21 -08:00
handler_name = "callback_%s" % resp.__class__.__name__
handler = getattr(self, handler_name, None)
2014-02-13 07:46:21 -08:00
if handler != None:
msg = handler(resp)
if msg == None:
2014-02-13 07:54:58 -08:00
raise Exception("Callback %s must return protobuf message, not None" % handler)
2014-02-13 07:46:21 -08:00
resp = self.call(msg)
finally:
self.transport.session_end()
return resp
def callback_Failure(self, msg):
if msg.code in (types.Failure_PinInvalid,
types.Failure_PinCancelled, types.Failure_PinExpected):
raise PinException(msg.code, msg.message)
raise CallException(msg.code, msg.message)
def close(self):
self.transport.close()
class DebugWireMixin(object):
def call_raw(self, msg):
log("SENDING " + pprint(msg))
resp = super(DebugWireMixin, self).call_raw(msg)
log("RECEIVED " + pprint(resp))
return resp
2014-02-13 07:46:21 -08:00
class TextUIMixin(object):
# This class demonstrates easy test-based UI
# integration between the device and wallet.
# You can implement similar functionality
# by implementing your own GuiMixin with
# graphical widgets for every type of these callbacks.
def __init__(self, *args, **kwargs):
super(TextUIMixin, self).__init__(*args, **kwargs)
2014-02-13 07:46:21 -08:00
def callback_ButtonRequest(self, msg):
# log("Sending ButtonAck for %s " % get_buttonrequest_value(msg.code))
2014-02-13 07:46:21 -08:00
return proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
2014-03-28 08:26:48 -07:00
if msg.type == 1:
2015-02-28 05:06:23 -08:00
desc = 'current PIN'
2014-03-28 08:26:48 -07:00
elif msg.type == 2:
2014-04-02 11:06:54 -07:00
desc = 'new PIN'
2014-03-28 08:26:48 -07:00
elif msg.type == 3:
2014-04-02 11:06:54 -07:00
desc = 'new PIN again'
2014-03-28 08:26:48 -07:00
else:
2014-04-02 11:06:54 -07:00
desc = 'PIN'
2015-08-21 06:16:27 -07:00
log("Use the numeric keypad to describe number positions. The layout is:")
log(" 7 8 9")
log(" 4 5 6")
log(" 1 2 3")
log("Please enter %s: " % desc)
pin = getpass.getpass('')
2014-02-13 07:46:21 -08:00
return proto.PinMatrixAck(pin=pin)
def callback_PassphraseRequest(self, msg):
log("Passphrase required: ")
passphrase = getpass.getpass('')
log("Confirm your Passphrase: ")
2014-11-23 04:28:09 -08:00
if passphrase == getpass.getpass(''):
passphrase = unicode(str(bytearray(passphrase, 'utf-8')), 'utf-8')
return proto.PassphraseAck(passphrase=passphrase)
else:
log("Passphrase did not match! ")
exit()
2014-02-13 07:46:21 -08:00
def callback_WordRequest(self, msg):
log("Enter one word of mnemonic: ")
word = raw_input()
2014-02-13 07:46:21 -08:00
return proto.WordAck(word=word)
2014-02-13 07:46:21 -08:00
class DebugLinkMixin(object):
# This class implements automatic responses
# and other functionality for unit tests
# for various callbacks, created in order
# to automatically pass unit tests.
#
# This mixing should be used only for purposes
# of unit testing, because it will fail to work
# without special DebugLink interface provided
# by the device.
def __init__(self, *args, **kwargs):
super(DebugLinkMixin, self).__init__(*args, **kwargs)
self.debug = None
self.in_with_statement = 0
2014-12-10 06:26:18 -08:00
self.button_wait = 0
self.screenshot_id = 0
2014-02-13 07:46:21 -08:00
# Always press Yes and provide correct pin
self.setup_debuglink(True, True)
2016-01-12 15:17:38 -08:00
# Do not expect any specific response from device
self.expected_responses = None
# Use blank passphrase
self.set_passphrase('')
2014-02-13 07:46:21 -08:00
def close(self):
super(DebugLinkMixin, self).close()
if self.debug:
self.debug.close()
def set_debuglink(self, debug_transport):
self.debug = DebugLink(debug_transport)
2014-12-10 06:26:18 -08:00
def set_buttonwait(self, secs):
self.button_wait = secs
def __enter__(self):
# For usage in with/expected_responses
self.in_with_statement += 1
return self
2014-02-21 12:00:56 -08:00
def __exit__(self, _type, value, traceback):
self.in_with_statement -= 1
2014-02-21 12:00:56 -08:00
if _type != None:
# Another exception raised
return False
# return isinstance(value, TypeError)
# Evaluate missed responses in 'with' statement
if self.expected_responses != None and len(self.expected_responses):
raise Exception("Some of expected responses didn't come from device: %s" % \
[ pprint(x) for x in self.expected_responses ])
# Cleanup
self.expected_responses = None
return False
def set_expected_responses(self, expected):
if not self.in_with_statement:
raise Exception("Must be called inside 'with' statement")
self.expected_responses = expected
2014-02-13 07:46:21 -08:00
def setup_debuglink(self, button, pin_correct):
self.button = button # True -> YES button, False -> NO button
self.pin_correct = pin_correct
def set_passphrase(self, passphrase):
2014-03-01 03:08:43 -08:00
self.passphrase = unicode(str(bytearray(Mnemonic.normalize_string(passphrase), 'utf-8')), 'utf-8')
2014-02-21 12:00:56 -08:00
def set_mnemonic(self, mnemonic):
2014-03-01 03:08:43 -08:00
self.mnemonic = unicode(str(bytearray(Mnemonic.normalize_string(mnemonic), 'utf-8')), 'utf-8').split(' ')
def call_raw(self, msg):
if SCREENSHOT and self.debug:
layout = self.debug.read_layout()
im = Image.new("RGB", (128, 64))
pix = im.load()
for x in range(128):
for y in range(64):
rx, ry = 127 - x, 63 - y
if (ord(layout[rx + (ry / 8) * 128]) & (1 << (ry % 8))) > 0:
pix[x, y] = (255, 255, 255)
im.save('scr%05d.png' % self.screenshot_id)
self.screenshot_id += 1
resp = super(DebugLinkMixin, self).call_raw(msg)
self._check_request(resp)
return resp
2016-01-12 15:17:38 -08:00
def _check_request(self, msg):
if self.expected_responses != None:
2014-02-13 07:46:21 -08:00
try:
expected = self.expected_responses.pop(0)
2014-02-13 07:46:21 -08:00
except IndexError:
raise CallException(types.Failure_Other,
"Got %s, but no message has been expected" % pprint(msg))
2014-02-13 07:46:21 -08:00
if msg.__class__ != expected.__class__:
raise CallException(types.Failure_Other,
"Expected %s, got %s" % (pprint(expected), pprint(msg)))
fields = expected.ListFields() # only filled (including extensions)
for field, value in fields:
if not msg.HasField(field.name) or getattr(msg, field.name) != value:
raise CallException(types.Failure_Other,
"Expected %s, got %s" % (pprint(expected), pprint(msg)))
2016-01-12 15:17:38 -08:00
def callback_ButtonRequest(self, msg):
log("ButtonRequest code: " + get_buttonrequest_value(msg.code))
2014-02-13 07:46:21 -08:00
2014-06-13 10:24:53 -07:00
log("Pressing button " + str(self.button))
2014-12-10 06:26:18 -08:00
if self.button_wait:
log("Waiting %d seconds " % self.button_wait)
time.sleep(self.button_wait)
2014-02-13 07:46:21 -08:00
self.debug.press_button(self.button)
return proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
if self.pin_correct:
pin = self.debug.read_pin_encoded()
else:
2014-02-13 07:46:21 -08:00
pin = '444222'
return proto.PinMatrixAck(pin=pin)
def callback_PassphraseRequest(self, msg):
log("Provided passphrase: '%s'" % self.passphrase)
return proto.PassphraseAck(passphrase=self.passphrase)
2014-02-13 07:46:21 -08:00
def callback_WordRequest(self, msg):
(word, pos) = self.debug.read_recovery_word()
2014-02-21 12:00:56 -08:00
if word != '':
return proto.WordAck(word=word)
if pos != 0:
return proto.WordAck(word=self.mnemonic[pos - 1])
raise Exception("Unexpected call")
2014-02-13 07:46:21 -08:00
class ProtocolMixin(object):
PRIME_DERIVATION_FLAG = 0x80000000
VENDORS = ('bitcointrezor.com', 'keepkey.com',)
2014-02-13 07:46:21 -08:00
def __init__(self, *args, **kwargs):
super(ProtocolMixin, self).__init__(*args, **kwargs)
self.init_device()
2014-03-28 11:47:53 -07:00
self.tx_api = None
2014-02-13 07:46:21 -08:00
2014-03-28 11:47:53 -07:00
def set_tx_api(self, tx_api):
self.tx_api = tx_api
2014-02-13 07:46:21 -08:00
def init_device(self):
self.features = expect(proto.Features)(self.call)(proto.Initialize())
2015-01-27 20:31:30 -08:00
if str(self.features.vendor) not in self.VENDORS:
raise Exception("Unsupported device")
def _get_local_entropy(self):
return os.urandom(32)
def _convert_prime(self, n):
# Convert minus signs to uint32 with flag
2014-02-13 07:46:21 -08:00
return [ int(abs(x) | self.PRIME_DERIVATION_FLAG) if x < 0 else x for x in n ]
2016-02-10 06:53:14 -08:00
@staticmethod
def expand_path(n):
# Convert string of bip32 path to list of uint32 integers with prime flags
# 0/-1/1' -> [0, 0x80000001, 0x80000001]
2014-01-09 15:11:03 -08:00
if not n:
return []
n = n.split('/')
path = []
for x in n:
prime = False
2014-01-09 15:11:03 -08:00
if x.endswith("'"):
x = x.replace('\'', '')
prime = True
2014-01-09 15:11:03 -08:00
if x.startswith('-'):
prime = True
2014-01-09 15:11:03 -08:00
x = abs(int(x))
if prime:
2016-02-10 06:53:14 -08:00
x |= ProtocolMixin.PRIME_DERIVATION_FLAG
2014-01-09 15:11:03 -08:00
path.append(x)
return path
2014-02-13 07:46:21 -08:00
@expect(proto.PublicKey)
def get_public_node(self, n, ecdsa_curve_name=DEFAULT_CURVE, show_display=False):
n = self._convert_prime(n)
if not ecdsa_curve_name:
ecdsa_curve_name=DEFAULT_CURVE
return self.call(proto.GetPublicKey(address_n=n, ecdsa_curve_name=ecdsa_curve_name, show_display=show_display))
2014-02-13 07:46:21 -08:00
@field('address')
@expect(proto.Address)
def get_address(self, coin_name, n, show_display=False, multisig=None):
n = self._convert_prime(n)
if multisig:
return self.call(proto.GetAddress(address_n=n, coin_name=coin_name, show_display=show_display, multisig=multisig))
else:
return self.call(proto.GetAddress(address_n=n, coin_name=coin_name, show_display=show_display))
2014-02-13 07:46:21 -08:00
@field('entropy')
@expect(proto.Entropy)
def get_entropy(self, size):
2014-02-13 07:46:21 -08:00
return self.call(proto.GetEntropy(size=size))
2014-02-13 07:46:21 -08:00
@field('message')
@expect(proto.Success)
def ping(self, msg, button_protection=False, pin_protection=False, passphrase_protection=False):
2014-02-03 15:31:44 -08:00
msg = proto.Ping(message=msg,
2014-02-03 16:02:16 -08:00
button_protection=button_protection,
pin_protection=pin_protection,
passphrase_protection=passphrase_protection)
2014-02-13 07:46:21 -08:00
return self.call(msg)
2013-10-08 11:33:39 -07:00
def get_device_id(self):
return self.features.device_id
2014-02-13 07:46:21 -08:00
@field('message')
@expect(proto.Success)
2015-02-04 11:53:22 -08:00
def apply_settings(self, label=None, language=None, use_passphrase=None, homescreen=None):
settings = proto.ApplySettings()
2014-01-09 15:11:03 -08:00
if label != None:
settings.label = label
if language:
settings.language = language
2014-12-13 10:07:30 -08:00
if use_passphrase != None:
settings.use_passphrase = use_passphrase
2015-02-04 11:53:22 -08:00
if homescreen != None:
settings.homescreen = homescreen
2014-02-13 07:46:21 -08:00
out = self.call(settings)
self.init_device() # Reload Features
return out
@field('message')
@expect(proto.Success)
def clear_session(self):
return self.call(proto.ClearSession())
2014-02-13 07:46:21 -08:00
@field('message')
@expect(proto.Success)
2014-01-31 10:48:19 -08:00
def change_pin(self, remove=False):
ret = self.call(proto.ChangePin(remove=remove))
self.init_device() # Re-read features
return ret
2014-02-13 07:46:21 -08:00
@expect(proto.MessageSignature)
def sign_message(self, coin_name, n, message):
2014-02-13 07:46:21 -08:00
n = self._convert_prime(n)
try:
# Convert message to UTF8 NFC (seems to be a bitcoin-qt standard)
message = normalize_nfc(message)
# Convert message to ASCII stream
message = str(bytearray(message, 'utf-8'))
except:
pass # it was not UTF8 string
return self.call(proto.SignMessage(coin_name=coin_name, address_n=n, message=message))
2014-02-13 07:46:21 -08:00
2015-02-20 09:50:53 -08:00
@expect(proto.SignedIdentity)
def sign_identity(self, identity, challenge_hidden, challenge_visual, ecdsa_curve_name=DEFAULT_CURVE):
return self.call(proto.SignIdentity(identity=identity, challenge_hidden=challenge_hidden, challenge_visual=challenge_visual, ecdsa_curve_name=ecdsa_curve_name))
2015-02-20 09:50:53 -08:00
2014-02-13 07:46:21 -08:00
def verify_message(self, address, signature, message):
try:
# Convert message to UTF8 NFC (seems to be a bitcoin-qt standard)
message = normalize_nfc(message)
# Convert message to ASCII stream
message = str(bytearray(message, 'utf-8'))
except:
pass # it was not UTF8 string
2014-02-13 07:46:21 -08:00
try:
if address:
resp = self.call(proto.VerifyMessage(address=address, signature=signature, message=message))
else:
resp = self.call(proto.VerifyMessage(signature=signature, message=message))
2014-02-13 07:46:21 -08:00
except CallException as e:
resp = e
if isinstance(resp, proto.Success):
return True
return False
2014-11-03 10:42:22 -08:00
@expect(proto.EncryptedMessage)
2014-10-29 16:48:06 -07:00
def encrypt_message(self, pubkey, message, display_only, coin_name, n):
if coin_name and n:
n = self._convert_prime(n)
return self.call(proto.EncryptMessage(pubkey=pubkey, message=message, display_only=display_only, coin_name=coin_name, address_n=n))
else:
return self.call(proto.EncryptMessage(pubkey=pubkey, message=message, display_only=display_only))
2014-06-12 08:02:46 -07:00
2014-11-03 10:42:22 -08:00
@expect(proto.DecryptedMessage)
def decrypt_message(self, n, nonce, message, msg_hmac):
2014-06-12 08:02:46 -07:00
n = self._convert_prime(n)
2014-11-03 10:42:22 -08:00
return self.call(proto.DecryptMessage(address_n=n, nonce=nonce, message=message, hmac=msg_hmac))
2014-06-12 08:02:46 -07:00
2014-11-03 10:42:22 -08:00
@field('value')
@expect(proto.CipheredKeyValue)
def encrypt_keyvalue(self, n, key, value, ask_on_encrypt=True, ask_on_decrypt=True, iv=None):
n = self._convert_prime(n)
return self.call(proto.CipherKeyValue(address_n=n,
key=key,
value=value,
encrypt=True,
ask_on_encrypt=ask_on_encrypt,
ask_on_decrypt=ask_on_decrypt,
iv=iv if iv is not None else ''))
2014-11-03 10:42:22 -08:00
@field('value')
@expect(proto.CipheredKeyValue)
def decrypt_keyvalue(self, n, key, value, ask_on_encrypt=True, ask_on_decrypt=True, iv=None):
n = self._convert_prime(n)
return self.call(proto.CipherKeyValue(address_n=n,
key=key,
value=value,
encrypt=False,
ask_on_encrypt=ask_on_encrypt,
ask_on_decrypt=ask_on_decrypt,
iv=iv if iv is not None else ''))
2014-02-13 07:46:21 -08:00
@field('tx_size')
@expect(proto.TxSize)
def estimate_tx_size(self, coin_name, inputs, outputs):
msg = proto.EstimateTxSize()
msg.coin_name = coin_name
msg.inputs_count = len(inputs)
msg.outputs_count = len(outputs)
return self.call(msg)
def _prepare_simple_sign_tx(self, coin_name, inputs, outputs):
msg = proto.SimpleSignTx()
msg.coin_name = coin_name
msg.inputs.extend(inputs)
msg.outputs.extend(outputs)
known_hashes = []
for inp in inputs:
if inp.prev_hash in known_hashes:
continue
tx = msg.transactions.add()
2015-06-03 05:53:53 -07:00
if self.tx_api:
tx.CopyFrom(self.tx_api.get_tx(binascii.hexlify(inp.prev_hash)))
else:
raise Exception('TX_API not defined')
2014-02-13 07:46:21 -08:00
known_hashes.append(inp.prev_hash)
return msg
def simple_sign_tx(self, coin_name, inputs, outputs):
msg = self._prepare_simple_sign_tx(coin_name, inputs, outputs)
return self.call(msg).serialized.serialized_tx
2014-04-09 12:42:30 -07:00
def _prepare_sign_tx(self, coin_name, inputs, outputs):
tx = types.TransactionType()
tx.inputs.extend(inputs)
2014-04-16 23:30:34 -07:00
tx.outputs.extend(outputs)
2014-04-09 12:42:30 -07:00
txes = {}
txes[''] = tx
known_hashes = []
for inp in inputs:
if inp.prev_hash in known_hashes:
continue
2015-06-03 05:53:53 -07:00
if self.tx_api:
txes[inp.prev_hash] = self.tx_api.get_tx(binascii.hexlify(inp.prev_hash))
else:
raise Exception('TX_API not defined')
2014-04-09 12:42:30 -07:00
known_hashes.append(inp.prev_hash)
return txes
2014-02-13 07:46:21 -08:00
def sign_tx(self, coin_name, inputs, outputs, debug_processor=None):
2014-04-09 12:42:30 -07:00
start = time.time()
txes = self._prepare_sign_tx(coin_name, inputs, outputs)
try:
self.transport.session_begin()
# Prepare and send initial message
tx = proto.SignTx()
tx.inputs_count = len(inputs)
tx.outputs_count = len(outputs)
tx.coin_name = coin_name
res = self.call(tx)
# Prepare structure for signatures
signatures = [None] * len(inputs)
serialized_tx = ''
counter = 0
while True:
counter += 1
if isinstance(res, proto.Failure):
raise CallException("Signing failed")
if not isinstance(res, proto.TxRequest):
raise CallException("Unexpected message")
# If there's some part of signed transaction, let's add it
2014-04-16 23:30:34 -07:00
if res.HasField('serialized') and res.serialized.HasField('serialized_tx'):
log("RECEIVED PART OF SERIALIZED TX (%d BYTES)" % len(res.serialized.serialized_tx))
2014-04-16 23:30:34 -07:00
serialized_tx += res.serialized.serialized_tx
2014-04-09 12:42:30 -07:00
2014-04-16 23:30:34 -07:00
if res.HasField('serialized') and res.serialized.HasField('signature_index'):
if signatures[res.serialized.signature_index] != None:
raise Exception("Signature for index %d already filled" % res.serialized.signature_index)
2014-04-16 23:30:34 -07:00
signatures[res.serialized.signature_index] = res.serialized.signature
2014-04-09 12:42:30 -07:00
if res.request_type == types.TXFINISHED:
# Device didn't ask for more information, finish workflow
break
# Device asked for one more information, let's process it.
2014-04-16 23:30:34 -07:00
current_tx = txes[res.details.tx_hash]
2014-04-09 12:42:30 -07:00
if res.request_type == types.TXMETA:
msg = types.TransactionType()
msg.version = current_tx.version
msg.lock_time = current_tx.lock_time
2014-04-17 05:05:45 -07:00
msg.inputs_cnt = len(current_tx.inputs)
2014-04-16 23:30:34 -07:00
if res.details.tx_hash:
2014-04-17 05:05:45 -07:00
msg.outputs_cnt = len(current_tx.bin_outputs)
2014-04-16 23:30:34 -07:00
else:
2014-04-17 05:05:45 -07:00
msg.outputs_cnt = len(current_tx.outputs)
2014-04-09 12:42:30 -07:00
res = self.call(proto.TxAck(tx=msg))
continue
elif res.request_type == types.TXINPUT:
msg = types.TransactionType()
2014-04-16 23:30:34 -07:00
msg.inputs.extend([current_tx.inputs[res.details.request_index], ])
2014-04-09 12:42:30 -07:00
res = self.call(proto.TxAck(tx=msg))
continue
elif res.request_type == types.TXOUTPUT:
msg = types.TransactionType()
2014-04-16 23:30:34 -07:00
if res.details.tx_hash:
msg.bin_outputs.extend([current_tx.bin_outputs[res.details.request_index], ])
else:
msg.outputs.extend([current_tx.outputs[res.details.request_index], ])
if debug_processor != None:
# If debug_processor function is provided,
# pass thru it the request and prepared response.
# This is useful for unit tests, see test_msg_signtx
msg = debug_processor(res, msg)
2014-04-09 12:42:30 -07:00
res = self.call(proto.TxAck(tx=msg))
continue
finally:
self.transport.session_end()
if None in signatures:
raise Exception("Some signatures are missing!")
log("SIGNED IN %.03f SECONDS, CALLED %d MESSAGES, %d BYTES" % \
(time.time() - start, counter, len(serialized_tx)))
2014-04-09 12:42:30 -07:00
return (signatures, serialized_tx)
2014-02-13 07:46:21 -08:00
@field('message')
@expect(proto.Success)
def wipe_device(self):
ret = self.call(proto.WipeDevice())
self.init_device()
return ret
@field('message')
@expect(proto.Success)
def recovery_device(self, word_count, passphrase_protection, pin_protection, label, language):
if self.features.initialized:
raise Exception("Device is initialized already. Call wipe_device() and try again.")
if word_count not in (12, 18, 24):
raise Exception("Invalid word count. Use 12/18/24")
res = self.call(proto.RecoveryDevice(word_count=int(word_count),
passphrase_protection=bool(passphrase_protection),
pin_protection=bool(pin_protection),
label=label,
language=language,
enforce_wordlist=True))
self.init_device()
return res
@field('message')
@expect(proto.Success)
def reset_device(self, display_random, strength, passphrase_protection, pin_protection, label, language):
if self.features.initialized:
raise Exception("Device is initialized already. Call wipe_device() and try again.")
# Begin with device reset workflow
msg = proto.ResetDevice(display_random=display_random,
strength=strength,
language=language,
passphrase_protection=bool(passphrase_protection),
pin_protection=bool(pin_protection),
label=label)
resp = self.call(msg)
if not isinstance(resp, proto.EntropyRequest):
raise Exception("Invalid response, expected EntropyRequest")
external_entropy = self._get_local_entropy()
log("Computer generated entropy: " + binascii.hexlify(external_entropy))
2014-02-21 12:00:56 -08:00
ret = self.call(proto.EntropyAck(entropy=external_entropy))
self.init_device()
return ret
2014-02-13 07:46:21 -08:00
@field('message')
@expect(proto.Success)
2014-02-20 16:48:11 -08:00
def load_device_by_mnemonic(self, mnemonic, pin, passphrase_protection, label, language, skip_checksum=False):
m = Mnemonic('english')
if not skip_checksum and not m.check(mnemonic):
raise Exception("Invalid mnemonic checksum")
# Convert mnemonic to UTF8 NKFD
mnemonic = Mnemonic.normalize_string(mnemonic)
# Convert mnemonic to ASCII stream
2014-03-01 03:08:43 -08:00
mnemonic = unicode(str(bytearray(mnemonic, 'utf-8')), 'utf-8')
2014-02-20 16:48:11 -08:00
2014-02-13 07:46:21 -08:00
if self.features.initialized:
raise Exception("Device is initialized already. Call wipe_device() and try again.")
resp = self.call(proto.LoadDevice(mnemonic=mnemonic, pin=pin,
passphrase_protection=passphrase_protection,
language=language,
2014-02-20 16:48:11 -08:00
label=label,
skip_checksum=skip_checksum))
2014-02-13 07:46:21 -08:00
self.init_device()
return resp
@field('message')
@expect(proto.Success)
2014-03-07 12:44:06 -08:00
def load_device_by_xprv(self, xprv, pin, passphrase_protection, label, language):
2014-02-13 07:46:21 -08:00
if self.features.initialized:
raise Exception("Device is initialized already. Call wipe_device() and try again.")
if xprv[0:4] not in ('xprv', 'tprv'):
raise Exception("Unknown type of xprv")
if len(xprv) < 100 and len(xprv) > 112:
raise Exception("Invalid length of xprv")
node = types.HDNodeType()
data = tools.b58decode(xprv, None).encode('hex')
if data[90:92] != '00':
raise Exception("Contain invalid private key")
checksum = hashlib.sha256(hashlib.sha256(binascii.unhexlify(data[:156])).digest()).hexdigest()[:8]
if checksum != data[156:]:
raise Exception("Checksum doesn't match")
# version 0488ade4
# depth 00
# fingerprint 00000000
# child_num 00000000
# chaincode 873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508
# privkey 00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35
# checksum e77e9d71
node.depth = int(data[8:10], 16)
node.fingerprint = int(data[10:18], 16)
node.child_num = int(data[18:26], 16)
node.chain_code = data[26:90].decode('hex')
node.private_key = data[92:156].decode('hex') # skip 0x00 indicating privkey
resp = self.call(proto.LoadDevice(node=node,
pin=pin,
passphrase_protection=passphrase_protection,
2014-03-07 12:44:06 -08:00
language=language,
2014-02-13 07:46:21 -08:00
label=label))
self.init_device()
return resp
def firmware_update(self, fp):
if self.features.bootloader_mode == False:
raise Exception("Device must be in bootloader mode")
resp = self.call(proto.FirmwareErase())
if isinstance(resp, proto.Failure) and resp.code == types.Failure_FirmwareError:
return False
2014-07-02 13:57:55 -07:00
data = fp.read()
fingerprint = hashlib.sha256(data[256:]).hexdigest()
log("Firmware fingerprint: " + fingerprint)
resp = self.call(proto.FirmwareUpload(payload=data))
2014-02-13 07:46:21 -08:00
if isinstance(resp, proto.Success):
return True
elif isinstance(resp, proto.Failure) and resp.code == types.Failure_FirmwareError:
return False
2014-08-11 16:25:36 -07:00
raise Exception("Unexpected result %s" % resp)
2014-02-13 07:46:21 -08:00
class TrezorClient(ProtocolMixin, TextUIMixin, BaseClient):
2014-02-13 07:46:21 -08:00
pass
class TrezorClientDebug(ProtocolMixin, TextUIMixin, DebugWireMixin, BaseClient):
pass
class TrezorDebugClient(ProtocolMixin, DebugLinkMixin, DebugWireMixin, BaseClient):
2014-02-13 07:46:21 -08:00
pass