From 681a52d3da474cbdb2974c8dfc559214bdf748dc Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Sat, 9 Feb 2019 22:26:42 -0500 Subject: [PATCH] Replace btchip-python with our own stripped down version of it --- hwilib/devices/btchip/README.md | 9 + hwilib/devices/btchip/__init__.py | 19 + hwilib/devices/btchip/bitcoinTransaction.py | 165 ++++++++ hwilib/devices/btchip/bitcoinVarint.py | 63 +++ hwilib/devices/btchip/btchip.py | 401 ++++++++++++++++++++ hwilib/devices/btchip/btchipComm.py | 147 +++++++ hwilib/devices/btchip/btchipException.py | 28 ++ hwilib/devices/btchip/btchipHelpers.py | 86 +++++ hwilib/devices/btchip/btchipUtils.py | 105 +++++ hwilib/devices/btchip/ledgerWrapper.py | 92 +++++ hwilib/devices/ledger.py | 4 +- setup.py | 1 - 12 files changed, 1117 insertions(+), 3 deletions(-) create mode 100644 hwilib/devices/btchip/README.md create mode 100644 hwilib/devices/btchip/__init__.py create mode 100644 hwilib/devices/btchip/bitcoinTransaction.py create mode 100644 hwilib/devices/btchip/bitcoinVarint.py create mode 100644 hwilib/devices/btchip/btchip.py create mode 100644 hwilib/devices/btchip/btchipComm.py create mode 100644 hwilib/devices/btchip/btchipException.py create mode 100644 hwilib/devices/btchip/btchipHelpers.py create mode 100644 hwilib/devices/btchip/btchipUtils.py create mode 100644 hwilib/devices/btchip/ledgerWrapper.py diff --git a/hwilib/devices/btchip/README.md b/hwilib/devices/btchip/README.md new file mode 100644 index 0000000..1693ce3 --- /dev/null +++ b/hwilib/devices/btchip/README.md @@ -0,0 +1,9 @@ +# Ledger Nano S Library + +This is a stripped down and modified version of the official [btchip-python](https://github.com/LedgerHQ/btchip-python) library. + +This stripped down version was made at commit [fe82d7f5638169f583a445b8e200fd1c9f3ea218](https://github.com/LedgerHQ/btchip-python/tree/fe82d7f5638169f583a445b8e200fd1c9f3ea218). + +## Changes + +- Removed support for Ledger HW.1 and other unused things diff --git a/hwilib/devices/btchip/__init__.py b/hwilib/devices/btchip/__init__.py new file mode 100644 index 0000000..598eef9 --- /dev/null +++ b/hwilib/devices/btchip/__init__.py @@ -0,0 +1,19 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + diff --git a/hwilib/devices/btchip/bitcoinTransaction.py b/hwilib/devices/btchip/bitcoinTransaction.py new file mode 100644 index 0000000..35276e9 --- /dev/null +++ b/hwilib/devices/btchip/bitcoinTransaction.py @@ -0,0 +1,165 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .bitcoinVarint import * +from binascii import hexlify + +class bitcoinInput: + + def __init__(self, bufferOffset=None): + self.prevOut = "" + self.script = "" + self.sequence = "" + if bufferOffset is not None: + buf = bufferOffset['buffer'] + offset = bufferOffset['offset'] + self.prevOut = buf[offset:offset + 36] + offset += 36 + scriptSize = readVarint(buf, offset) + offset += scriptSize['size'] + self.script = buf[offset:offset + scriptSize['value']] + offset += scriptSize['value'] + self.sequence = buf[offset:offset + 4] + offset += 4 + bufferOffset['offset'] = offset + + def serialize(self): + result = [] + result.extend(self.prevOut) + writeVarint(len(self.script), result) + result.extend(self.script) + result.extend(self.sequence) + return result + + def __str__(self): + buf = "Prevout : " + hexlify(self.prevOut) + "\r\n" + buf += "Script : " + hexlify(self.script) + "\r\n" + buf += "Sequence : " + hexlify(self.sequence) + "\r\n" + return buf + +class bitcoinOutput: + + def __init__(self, bufferOffset=None): + self.amount = "" + self.script = "" + if bufferOffset is not None: + buf = bufferOffset['buffer'] + offset = bufferOffset['offset'] + self.amount = buf[offset:offset + 8] + offset += 8 + scriptSize = readVarint(buf, offset) + offset += scriptSize['size'] + self.script = buf[offset:offset + scriptSize['value']] + offset += scriptSize['value'] + bufferOffset['offset'] = offset + + def serialize(self): + result = [] + result.extend(self.amount) + writeVarint(len(self.script), result) + result.extend(self.script) + return result + + def __str__(self): + buf = "Amount : " + hexlify(self.amount) + "\r\n" + buf += "Script : " + hexlify(self.script) + "\r\n" + return buf + + +class bitcoinTransaction: + + def __init__(self, data=None): + self.version = "" + self.inputs = [] + self.outputs = [] + self.lockTime = "" + self.witness = False + self.witnessScript = "" + if data is not None: + offset = 0 + self.version = data[offset:offset + 4] + offset += 4 + if (data[offset] == 0) and (data[offset + 1] != 0): + offset += 2 + self.witness = True + inputSize = readVarint(data, offset) + offset += inputSize['size'] + numInputs = inputSize['value'] + for i in range(numInputs): + tmp = { 'buffer': data, 'offset' : offset} + self.inputs.append(bitcoinInput(tmp)) + offset = tmp['offset'] + outputSize = readVarint(data, offset) + offset += outputSize['size'] + numOutputs = outputSize['value'] + for i in range(numOutputs): + tmp = { 'buffer': data, 'offset' : offset} + self.outputs.append(bitcoinOutput(tmp)) + offset = tmp['offset'] + if self.witness: + self.witnessScript = data[offset : len(data) - 4] + self.lockTime = data[len(data) - 4:] + else: + self.lockTime = data[offset:offset + 4] + + def serialize(self, skipOutputLocktime=False, skipWitness=False): + if skipWitness or (not self.witness): + useWitness = False + else: + useWitness = True + result = [] + result.extend(self.version) + if useWitness: + result.append(0x00) + result.append(0x01) + writeVarint(len(self.inputs), result) + for trinput in self.inputs: + result.extend(trinput.serialize()) + if not skipOutputLocktime: + writeVarint(len(self.outputs), result) + for troutput in self.outputs: + result.extend(troutput.serialize()) + if useWitness: + result.extend(self.witnessScript) + result.extend(self.lockTime) + return result + + def serializeOutputs(self): + result = [] + writeVarint(len(self.outputs), result) + for troutput in self.outputs: + result.extend(troutput.serialize()) + return result + + def __str__(self): + buf = "Version : " + hexlify(self.version) + "\r\n" + index = 1 + for trinput in self.inputs: + buf += "Input #" + str(index) + "\r\n" + buf += str(trinput) + index+=1 + index = 1 + for troutput in self.outputs: + buf += "Output #" + str(index) + "\r\n" + buf += str(troutput) + index+=1 + buf += "Locktime : " + hexlify(self.lockTime) + "\r\n" + if self.witness: + buf += "Witness script : " + hexlify(self.witnessScript) + "\r\n" + return buf diff --git a/hwilib/devices/btchip/bitcoinVarint.py b/hwilib/devices/btchip/bitcoinVarint.py new file mode 100644 index 0000000..a0dd868 --- /dev/null +++ b/hwilib/devices/btchip/bitcoinVarint.py @@ -0,0 +1,63 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .btchipException import BTChipException + +def readVarint(buffer, offset): + varintSize = 0 + value = 0 + if (buffer[offset] < 0xfd): + value = buffer[offset] + varintSize = 1 + elif (buffer[offset] == 0xfd): + value = (buffer[offset + 2] << 8) | (buffer[offset + 1]) + varintSize = 3 + elif (buffer[offset] == 0xfe): + value = (buffer[offset + 4] << 24) | (buffer[offset + 3] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 1]) + varintSize = 5 + else: + raise BTChipException("unsupported varint") + return { "value": value, "size": varintSize } + +def writeVarint(value, buffer): + if (value < 0xfd): + buffer.append(value) + elif (value <= 0xffff): + buffer.append(0xfd) + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + elif (value <= 0xffffffff): + buffer.append(0xfe) + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 24) & 0xff) + else: + raise BTChipException("unsupported encoding") + return buffer + +def getVarintSize(value): + if (value < 0xfd): + return 1 + elif (value <= 0xffff): + return 3 + elif (value <= 0xffffffff): + return 5 + else: + raise BTChipException("unsupported encoding") diff --git a/hwilib/devices/btchip/btchip.py b/hwilib/devices/btchip/btchip.py new file mode 100644 index 0000000..3627f66 --- /dev/null +++ b/hwilib/devices/btchip/btchip.py @@ -0,0 +1,401 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .btchipComm import * +from .bitcoinTransaction import * +from .bitcoinVarint import * +from .btchipException import * +from .btchipHelpers import * +from binascii import hexlify, unhexlify + +class btchip: + BTCHIP_CLA = 0xe0 + BTCHIP_JC_EXT_CLA = 0xf0 + + BTCHIP_INS_SET_ALTERNATE_COIN_VERSION = 0x14 + BTCHIP_INS_SETUP = 0x20 + BTCHIP_INS_VERIFY_PIN = 0x22 + BTCHIP_INS_GET_OPERATION_MODE = 0x24 + BTCHIP_INS_SET_OPERATION_MODE = 0x26 + BTCHIP_INS_SET_KEYMAP = 0x28 + BTCHIP_INS_SET_COMM_PROTOCOL = 0x2a + BTCHIP_INS_GET_WALLET_PUBLIC_KEY = 0x40 + BTCHIP_INS_GET_TRUSTED_INPUT = 0x42 + BTCHIP_INS_HASH_INPUT_START = 0x44 + BTCHIP_INS_HASH_INPUT_FINALIZE = 0x46 + BTCHIP_INS_HASH_SIGN = 0x48 + BTCHIP_INS_HASH_INPUT_FINALIZE_FULL = 0x4a + BTCHIP_INS_GET_INTERNAL_CHAIN_INDEX = 0x4c + BTCHIP_INS_SIGN_MESSAGE = 0x4e + BTCHIP_INS_GET_TRANSACTION_LIMIT = 0xa0 + BTCHIP_INS_SET_TRANSACTION_LIMIT = 0xa2 + BTCHIP_INS_IMPORT_PRIVATE_KEY = 0xb0 + BTCHIP_INS_GET_PUBLIC_KEY = 0xb2 + BTCHIP_INS_DERIVE_BIP32_KEY = 0xb4 + BTCHIP_INS_SIGNVERIFY_IMMEDIATE = 0xb6 + BTCHIP_INS_GET_RANDOM = 0xc0 + BTCHIP_INS_GET_ATTESTATION = 0xc2 + BTCHIP_INS_GET_FIRMWARE_VERSION = 0xc4 + BTCHIP_INS_COMPOSE_MOFN_ADDRESS = 0xc6 + BTCHIP_INS_GET_POS_SEED = 0xca + + BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY = 0x20 + BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY = 0x22 + BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY = 0x24 + BTCHIP_INS_EXT_CACHE_GET_FEATURES = 0x26 + + OPERATION_MODE_WALLET = 0x01 + OPERATION_MODE_RELAXED_WALLET = 0x02 + OPERATION_MODE_SERVER = 0x04 + OPERATION_MODE_DEVELOPER = 0x08 + + FEATURE_UNCOMPRESSED_KEYS = 0x01 + FEATURE_RFC6979 = 0x02 + FEATURE_FREE_SIGHASHTYPE = 0x04 + FEATURE_NO_2FA_P2SH = 0x08 + + QWERTY_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f313035")) + QWERTZ_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f313035")) + AZERTY_KEYMAP = bytearray(unhexlify("08000000010000200100007820c8ffc3feffff07000000002c38202030341e21222d352e102e3637271e1f202122232425263736362e37101f1405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f64302f2d351405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f643035")) + + def __init__(self, dongle): + self.dongle = dongle + self.needKeyCache = False + try: + firmware = self.getFirmwareVersion()['version'] + self.multiOutputSupported = tuple(map(int, (firmware.split(".")))) >= (1, 1, 4) + if self.multiOutputSupported: + self.scriptBlockLength = 50 + else: + self.scriptBlockLength = 255 + except: + pass + + def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): + result = {} + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_WALLET_PUBLIC_KEY, 0x01 if showOnScreen else 0x00, 0x03 if cashAddr else 0x02 if segwitNative else 0x01 if segwit else 0x00, len(donglePath) ] + apdu.extend(donglePath) + response = self.dongle.exchange(bytearray(apdu)) + offset = 0 + result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] + offset = offset + 1 + response[offset] + result['address'] = str(response[offset + 1 : offset + 1 + response[offset]]) + offset = offset + 1 + response[offset] + result['chainCode'] = response[offset : offset + 32] + return result + + def getTrustedInput(self, transaction, index): + result = {} + # Header + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x00, 0x00 ] + params = bytearray.fromhex("%.8x" % (index)) + params.extend(transaction.version) + writeVarint(len(transaction.inputs), params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + # Each input + for trinput in transaction.inputs: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] + params = bytearray(trinput.prevOut) + writeVarint(len(trinput.script), params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset = 0 + while True: + blockLength = 251 + if ((offset + blockLength) < len(trinput.script)): + dataLength = blockLength + else: + dataLength = len(trinput.script) - offset + params = bytearray(trinput.script[offset : offset + dataLength]) + if ((offset + dataLength) == len(trinput.script)): + params.extend(trinput.sequence) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(params) ] + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset += dataLength + if (offset >= len(trinput.script)): + break + # Number of outputs + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] + params = [] + writeVarint(len(transaction.outputs), params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + # Each output + indexOutput = 0 + for troutput in transaction.outputs: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] + params = bytearray(troutput.amount) + writeVarint(len(troutput.script), params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset = 0 + while (offset < len(troutput.script)): + blockLength = 255 + if ((offset + blockLength) < len(troutput.script)): + dataLength = blockLength + else: + dataLength = len(troutput.script) - offset + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, dataLength ] + apdu.extend(troutput.script[offset : offset + dataLength]) + self.dongle.exchange(bytearray(apdu)) + offset += dataLength + # Locktime + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(transaction.lockTime) ] + apdu.extend(transaction.lockTime) + response = self.dongle.exchange(bytearray(apdu)) + result['trustedInput'] = True + result['value'] = response + return result + + def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, redeemScript, version=0x01, cashAddr=False): + # Start building a fake transaction with the passed inputs + segwit = False + if newTransaction: + for passedOutput in outputList: + if ('witness' in passedOutput) and passedOutput['witness']: + segwit = True + break + if newTransaction: + if segwit: + p2 = 0x03 if cashAddr else 0x02 + else: + p2 = 0x00 + else: + p2 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ] + params = bytearray([version, 0x00, 0x00, 0x00]) + writeVarint(len(outputList), params) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + # Loop for each input + currentIndex = 0 + for passedOutput in outputList: + if ('sequence' in passedOutput) and passedOutput['sequence']: + sequence = bytearray(unhexlify(passedOutput['sequence'])) + else: + sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) # default sequence + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ] + params = [] + script = bytearray(redeemScript) + if ('witness' in passedOutput) and passedOutput['witness']: + params.append(0x02) + elif ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + params.append(0x01) + else: + params.append(0x00) + if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: + params.append(len(passedOutput['value'])) + params.extend(passedOutput['value']) + if currentIndex != inputIndex: + script = bytearray() + writeVarint(len(script), params) + if len(script) == 0: + params.extend(sequence) + apdu.append(len(params)) + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset = 0 + while(offset < len(script)): + blockLength = 255 + if ((offset + blockLength) < len(script)): + dataLength = blockLength + else: + dataLength = len(script) - offset + params = script[offset : offset + dataLength] + if ((offset + dataLength) == len(script)): + params.extend(sequence) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(params) ] + apdu.extend(params) + self.dongle.exchange(bytearray(apdu)) + offset += blockLength + currentIndex += 1 + + def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): + alternateEncoding = False + donglePath = parse_bip32_path(changePath) + if self.needKeyCache: + self.resolvePublicKeysInPath(changePath) + result = {} + outputs = None + if rawTx is not None: + try: + fullTx = bitcoinTransaction(bytearray(rawTx)) + outputs = fullTx.serializeOutputs() + if len(donglePath) != 0: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00 ] + params = [] + params.extend(donglePath) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + offset = 0 + while (offset < len(outputs)): + blockLength = self.scriptBlockLength + if ((offset + blockLength) < len(outputs)): + dataLength = blockLength + p1 = 0x00 + else: + dataLength = len(outputs) - offset + p1 = 0x80 + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ + p1, 0x00, dataLength ] + apdu.extend(outputs[offset : offset + dataLength]) + response = self.dongle.exchange(bytearray(apdu)) + offset += dataLength + alternateEncoding = True + except: + pass + if not alternateEncoding: + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ] + params = [] + params.append(len(outputAddress)) + params.extend(bytearray(outputAddress)) + writeHexAmountBE(btc_to_satoshi(str(amount)), params) + writeHexAmountBE(btc_to_satoshi(str(fees)), params) + params.extend(donglePath) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x02: + result['keycardData'] = response[1 + response[0] + 1:] + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + offset = offset + 1 + result['keycardData'] = response[offset : offset + keycardDataLength] + offset = offset + keycardDataLength + result['secureScreenData'] = response[offset:] + if result['confirmationType'] == 0x04: + offset = 1 + response[0] + 1 + keycardDataLength = response[offset] + result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] + if outputs == None: + result['outputData'] = response[1 : 1 + response[0]] + else: + result['outputData'] = outputs + return result + + def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01): + if isinstance(pin, str): + pin = pin.encode('utf-8') + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_SIGN, 0x00, 0x00 ] + params = [] + params.extend(donglePath) + params.append(len(pin)) + params.extend(bytearray(pin)) + writeUint32BE(lockTime, params) + params.append(sighashType) + apdu.append(len(params)) + apdu.extend(params) + result = self.dongle.exchange(bytearray(apdu)) + result[0] = 0x30 + return result + + def signMessagePrepareV2(self, path, message): + donglePath = parse_bip32_path(path) + if self.needKeyCache: + self.resolvePublicKeysInPath(path) + result = {} + offset = 0 + encryptedOutputData = b"" + while (offset < len(message)): + params = []; + if offset == 0: + params.extend(donglePath) + params.append((len(message) >> 8) & 0xff) + params.append(len(message) & 0xff) + p2 = 0x01 + else: + p2 = 0x80 + blockLength = 255 - len(params) + if ((offset + blockLength) < len(message)): + dataLength = blockLength + else: + dataLength = len(message) - offset + params.extend(bytearray(message[offset : offset + dataLength])) + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, p2 ] + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] + offset += blockLength + result['confirmationNeeded'] = response[1 + response[0]] != 0x00 + result['confirmationType'] = response[1 + response[0]] + if result['confirmationType'] == 0x03: + offset = 1 + response[0] + 1 + result['secureScreenData'] = response[offset:] + result['encryptedOutputData'] = encryptedOutputData + + return result + + def signMessagePrepare(self, path, message): + try: + result = self.signMessagePrepareV2(path, message) + except BTChipException as e: + if (e.sw == 0x6b00): # Old firmware version, try older method + result = self.signMessagePrepareV1(path, message) + else: + raise + return result + + def signMessageSign(self, pin=""): + if isinstance(pin, str): + pin = pin.encode('utf-8') + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x80, 0x00 ] + params = [] + if pin is not None: + params.append(len(pin)) + params.extend(bytearray(pin)) + else: + params.append(0x00) + apdu.append(len(params)) + apdu.extend(params) + response = self.dongle.exchange(bytearray(apdu)) + return response + + def getFirmwareVersion(self): + result = {} + apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_FIRMWARE_VERSION, 0x00, 0x00, 0x00 ] + try: + response = self.dongle.exchange(bytearray(apdu)) + except BTChipException as e: + if (e.sw == 0x6985): + response = [0x00, 0x00, 0x01, 0x04, 0x03 ] + pass + else: + raise + result['compressedKeys'] = (response[0] == 0x01) + result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) + result['specialVersion'] = response[1] + return result diff --git a/hwilib/devices/btchip/btchipComm.py b/hwilib/devices/btchip/btchipComm.py new file mode 100644 index 0000000..bc878b8 --- /dev/null +++ b/hwilib/devices/btchip/btchipComm.py @@ -0,0 +1,147 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from abc import ABCMeta, abstractmethod +from .btchipException import * +from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU +from binascii import hexlify +import time +import os +import struct +import socket + +try: + import hid + HID = True +except ImportError: + HID = False + +try: + from smartcard.Exceptions import NoCardException + from smartcard.System import readers + from smartcard.util import toHexString, toBytes + SCARD = True +except ImportError: + SCARD = False + +class DongleWait(object): + __metaclass__ = ABCMeta + + @abstractmethod + def waitFirstResponse(self, timeout): + pass + +class Dongle(object): + __metaclass__ = ABCMeta + + @abstractmethod + def exchange(self, apdu, timeout=20000): + pass + + @abstractmethod + def close(self): + pass + + def setWaitImpl(self, waitImpl): + self.waitImpl = waitImpl + +class HIDDongleHIDAPI(Dongle, DongleWait): + + def __init__(self, device, ledger=False, debug=False): + self.device = device + self.ledger = ledger + self.debug = debug + self.waitImpl = self + self.opened = True + + def exchange(self, apdu, timeout=20000): + if self.debug: + print("=> %s" % hexlify(apdu)) + if self.ledger: + apdu = wrapCommandAPDU(0x0101, apdu, 64) + padSize = len(apdu) % 64 + tmp = apdu + if padSize != 0: + tmp.extend([0] * (64 - padSize)) + offset = 0 + while(offset != len(tmp)): + data = tmp[offset:offset + 64] + data = bytearray([0]) + data + self.device.write(data) + offset += 64 + dataLength = 0 + dataStart = 2 + result = self.waitImpl.waitFirstResponse(timeout) + if not self.ledger: + if result[0] == 0x61: # 61xx : data available + self.device.set_nonblocking(False) + dataLength = result[1] + dataLength += 2 + if dataLength > 62: + remaining = dataLength - 62 + while(remaining != 0): + if remaining > 64: + blockLength = 64 + else: + blockLength = remaining + result.extend(bytearray(self.device.read(65))[0:blockLength]) + remaining -= blockLength + swOffset = dataLength + dataLength -= 2 + self.device.set_nonblocking(True) + else: + swOffset = 0 + else: + self.device.set_nonblocking(False) + while True: + response = unwrapResponseAPDU(0x0101, result, 64) + if response is not None: + result = response + dataStart = 0 + swOffset = len(response) - 2 + dataLength = len(response) - 2 + self.device.set_nonblocking(True) + break + result.extend(bytearray(self.device.read(65))) + sw = (result[swOffset] << 8) + result[swOffset + 1] + response = result[dataStart : dataLength + dataStart] + if self.debug: + print("<= %s%.2x" % (hexlify(response), sw)) + if sw != 0x9000: + raise BTChipException("Invalid status %04x" % sw, sw) + return response + + def waitFirstResponse(self, timeout): + start = time.time() + data = "" + while len(data) == 0: + data = self.device.read(65) + if not len(data): + if time.time() - start > timeout: + raise BTChipException("Timeout") + time.sleep(0.02) + return bytearray(data) + + def close(self): + if self.opened: + try: + self.device.close() + except: + pass + self.opened = False diff --git a/hwilib/devices/btchip/btchipException.py b/hwilib/devices/btchip/btchipException.py new file mode 100644 index 0000000..ec57728 --- /dev/null +++ b/hwilib/devices/btchip/btchipException.py @@ -0,0 +1,28 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +class BTChipException(Exception): + + def __init__(self, message, sw=0x6f00): + self.message = message + self.sw = sw + + def __str__(self): + buf = "Exception : " + self.message + return buf diff --git a/hwilib/devices/btchip/btchipHelpers.py b/hwilib/devices/btchip/btchipHelpers.py new file mode 100644 index 0000000..ba5b66c --- /dev/null +++ b/hwilib/devices/btchip/btchipHelpers.py @@ -0,0 +1,86 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +import decimal +import re + +# from pycoin +SATOSHI_PER_COIN = decimal.Decimal(1e8) +COIN_PER_SATOSHI = decimal.Decimal(1)/SATOSHI_PER_COIN + +def satoshi_to_btc(satoshi_count): + if satoshi_count == 0: + return decimal.Decimal(0) + r = satoshi_count * COIN_PER_SATOSHI + return r.normalize() + +def btc_to_satoshi(btc): + return int(decimal.Decimal(btc) * SATOSHI_PER_COIN) +# /from pycoin + +def writeUint32BE(value, buffer): + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append(value & 0xff) + return buffer + +def writeUint32LE(value, buffer): + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 24) & 0xff) + return buffer + +def writeHexAmount(value, buffer): + buffer.append(value & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 32) & 0xff) + buffer.append((value >> 40) & 0xff) + buffer.append((value >> 48) & 0xff) + buffer.append((value >> 56) & 0xff) + return buffer + +def writeHexAmountBE(value, buffer): + buffer.append((value >> 56) & 0xff) + buffer.append((value >> 48) & 0xff) + buffer.append((value >> 40) & 0xff) + buffer.append((value >> 32) & 0xff) + buffer.append((value >> 24) & 0xff) + buffer.append((value >> 16) & 0xff) + buffer.append((value >> 8) & 0xff) + buffer.append(value & 0xff) + return buffer + +def parse_bip32_path(path): + if len(path) == 0: + return bytearray([ 0 ]) + result = [] + elements = path.split('/') + if len(elements) > 10: + raise BTChipException("Path too long") + for pathElement in elements: + element = re.split('\'|h|H', pathElement) + if len(element) == 1: + writeUint32BE(int(element[0]), result) + else: + writeUint32BE(0x80000000 | int(element[0]), result) + return bytearray([ len(elements) ] + result) diff --git a/hwilib/devices/btchip/btchipUtils.py b/hwilib/devices/btchip/btchipUtils.py new file mode 100644 index 0000000..0bf2fa2 --- /dev/null +++ b/hwilib/devices/btchip/btchipUtils.py @@ -0,0 +1,105 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +from .btchipException import * +from .bitcoinTransaction import * +from .btchipHelpers import * + +def compress_public_key(publicKey): + if publicKey[0] == 0x04: + if (publicKey[64] & 1) != 0: + prefix = 0x03 + else: + prefix = 0x02 + result = [prefix] + result.extend(publicKey[1:33]) + return bytearray(result) + elif publicKey[0] == 0x03 or publicKey[0] == 0x02: + return publicKey + else: + raise BTChipException("Invalid public key format") + +def format_transaction(dongleOutputData, trustedInputsAndInputScripts, version=0x01, lockTime=0): + transaction = bitcoinTransaction() + transaction.version = [] + writeUint32LE(version, transaction.version) + for item in trustedInputsAndInputScripts: + newInput = bitcoinInput() + newInput.prevOut = item[0][4:4+36] + newInput.script = item[1] + if len(item) > 2: + newInput.sequence = bytearray(item[2].decode('hex')) + else: + newInput.sequence = bytearray([0xff, 0xff, 0xff, 0xff]) + transaction.inputs.append(newInput) + result = transaction.serialize(True) + result.extend(dongleOutputData) + writeUint32LE(lockTime, result) + return bytearray(result) + +def get_regular_input_script(sigHashtype, publicKey): + if len(sigHashtype) >= 0x4c: + raise BTChipException("Invalid sigHashtype") + if len(publicKey) >= 0x4c: + raise BTChipException("Invalid publicKey") + result = [ len(sigHashtype) ] + result.extend(sigHashtype) + result.append(len(publicKey)) + result.extend(publicKey) + return bytearray(result) + +def write_pushed_data_size(data, buffer): + if (len(data) > 0xffff): + raise BTChipException("unsupported encoding") + if (len(data) < 0x4c): + buffer.append(len(data)) + elif (len(data) > 255): + buffer.append(0x4d) + buffer.append(len(data) & 0xff) + buffer.append((len(data) >> 8) & 0xff) + else: + buffer.append(0x4c) + buffer.append(len(data)) + return buffer + + +def get_p2sh_input_script(redeemScript, sigHashtypeList): + result = [ 0x00 ] + for sigHashtype in sigHashtypeList: + write_pushed_data_size(sigHashtype, result) + result.extend(sigHashtype) + write_pushed_data_size(redeemScript, result) + result.extend(redeemScript) + return bytearray(result) + +def get_p2pk_input_script(sigHashtype): + if len(sigHashtype) >= 0x4c: + raise BTChipException("Invalid sigHashtype") + result = [ len(sigHashtype) ] + result.extend(sigHashtype) + return bytearray(result) + +def get_output_script(amountScriptArray): + result = [ len(amountScriptArray) ] + for amountScript in amountScriptArray: + writeHexAmount(btc_to_satoshi(str(amountScript[0])), result) + writeVarint(len(amountScript[1]), result) + result.extend(amountScript[1]) + return bytearray(result) + diff --git a/hwilib/devices/btchip/ledgerWrapper.py b/hwilib/devices/btchip/ledgerWrapper.py new file mode 100644 index 0000000..44fde1d --- /dev/null +++ b/hwilib/devices/btchip/ledgerWrapper.py @@ -0,0 +1,92 @@ +""" +******************************************************************************* +* BTChip Bitcoin Hardware Wallet Python API +* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +import struct +from .btchipException import BTChipException + +def wrapCommandAPDU(channel, command, packetSize): + if packetSize < 3: + raise BTChipException("Can't handle Ledger framing with less than 3 bytes for the report") + sequenceIdx = 0 + offset = 0 + result = struct.pack(">HBHH", channel, 0x05, sequenceIdx, len(command)) + sequenceIdx = sequenceIdx + 1 + if len(command) > packetSize - 7: + blockSize = packetSize - 7 + else: + blockSize = len(command) + result += command[offset : offset + blockSize] + offset = offset + blockSize + while offset != len(command): + result += struct.pack(">HBH", channel, 0x05, sequenceIdx) + sequenceIdx = sequenceIdx + 1 + if (len(command) - offset) > packetSize - 5: + blockSize = packetSize - 5 + else: + blockSize = len(command) - offset + result += command[offset : offset + blockSize] + offset = offset + blockSize + while (len(result) % packetSize) != 0: + result += b"\x00" + return bytearray(result) + +def unwrapResponseAPDU(channel, data, packetSize): + sequenceIdx = 0 + offset = 0 + if ((data is None) or (len(data) < 7 + 5)): + return None + if struct.unpack(">H", data[offset : offset + 2])[0] != channel: + raise BTChipException("Invalid channel") + offset += 2 + if data[offset] != 0x05: + raise BTChipException("Invalid tag") + offset += 1 + if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: + raise BTChipException("Invalid sequence") + offset += 2 + responseLength = struct.unpack(">H", data[offset : offset + 2])[0] + offset += 2 + if len(data) < 7 + responseLength: + return None + if responseLength > packetSize - 7: + blockSize = packetSize - 7 + else: + blockSize = responseLength + result = data[offset : offset + blockSize] + offset += blockSize + while (len(result) != responseLength): + sequenceIdx = sequenceIdx + 1 + if (offset == len(data)): + return None + if struct.unpack(">H", data[offset : offset + 2])[0] != channel: + raise BTChipException("Invalid channel") + offset += 2 + if data[offset] != 0x05: + raise BTChipException("Invalid tag") + offset += 1 + if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: + raise BTChipException("Invalid sequence") + offset += 2 + if (responseLength - len(result)) > packetSize - 5: + blockSize = packetSize - 5 + else: + blockSize = responseLength - len(result) + result += data[offset : offset + blockSize] + offset += blockSize + return bytearray(result) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 533f8d3..7ab94a4 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -2,8 +2,8 @@ from ..hwwclient import HardwareWalletClient from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, UnavailableActionError -from btchip.btchip import * -from btchip.btchipUtils import * +from .btchip.btchip import * +from .btchip.btchipUtils import * import base64 import json import struct diff --git a/setup.py b/setup.py index 8e29a91..ef012ce 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ setuptools.setup( packages=setuptools.find_packages(exclude=['docs', 'test']), install_requires=[ 'hidapi', # HID API needed in general - 'btchip-python', # Ledger Nano S 'pyaes', 'ecdsa', # Needed for Ledger but their library does not install it 'typing_extensions>=3.7',