def write_file_encrypted(file_name: str, hw_session: HwSessionInfo, data: bytes): label = os.path.basename(file_name) if hw_session.app_config.hw_type: if not hw_session.hw_client: if not hw_session.hw_connect(): return else: raise Exception('Invalid hardware wallet type in the app configuration.') protocol, hw_type_bin, bip32_path_n, encryption_key, encrypted_key_bin, pub_key_hash = \ prepare_hw_encryption_attrs(hw_session, label) fer = Fernet(encryption_key) with open(file_name, 'wb') as f_ptr: header = HMT_ENCRYPTED_DATA_PREFIX + \ num_to_varint(protocol) + num_to_varint(hw_type_bin) + \ write_bytes_buf(bytearray(base64.b64encode(bytearray(label, 'utf-8')))) + \ write_bytes_buf(encrypted_key_bin) + \ write_int_list_buf(bip32_path_n) + \ write_bytes_buf(pub_key_hash) f_ptr.write(header) # slice the input data into ENC_FILE_BLOCK_SIZE-byte chunks, encrypt them and # write to file; each block will be preceded with the length of the encrypted # data chunk size begin_idx = 0 while True: data_left = len(data) - begin_idx if data_left <= 0: break cur_input_chunk_size = min(ENC_FILE_BLOCK_SIZE, data_left) data_enc_base64 = fer.encrypt(data[begin_idx : begin_idx+cur_input_chunk_size]) data_enc = base64.urlsafe_b64decode(data_enc_base64) cur_chunk_size_bin = len(data_enc).to_bytes(8, byteorder='little') # write the size of the data chunk f_ptr.write(cur_chunk_size_bin) # write the data f_ptr.write(data_enc) # write the data begin_idx += cur_input_chunk_size
def get_tx(self, txhash): data = self.fetch_json(self.url, 'tx', txhash) t = proto_types.TransactionType() t.version = data['version'] t.lock_time = data['locktime'] for vin in data['vin']: i = t.inputs.add() if 'coinbase' in vin.keys(): i.prev_hash = b"\0" * 32 i.prev_index = 0xffffffff # signed int -1 i.script_sig = binascii.unhexlify(vin['coinbase']) i.sequence = vin['sequence'] else: i.prev_hash = binascii.unhexlify(vin['txid']) i.prev_index = vin['vout'] i.script_sig = binascii.unhexlify(vin['scriptSig']['hex']) i.sequence = vin['sequence'] for vout in data['vout']: o = t.bin_outputs.add() o.amount = int(Decimal(str(vout['value'])) * 100000000) o.script_pubkey = binascii.unhexlify(vout['scriptPubKey']['hex']) dip2_type = data.get("type", 0) if t.version == 3 and dip2_type != 0: # It's a DIP2 special TX with payload if "extraPayloadSize" not in data or "extraPayload" not in data: raise ValueError("Payload data missing in DIP2 transaction") if data["extraPayloadSize"] * 2 != len(data["extraPayload"]): raise ValueError( "extra_data_len (%d) does not match calculated length (%d)" % (data["extraPayloadSize"], len(data["extraPayload"]) * 2)) t.extra_data = hatch_utils.num_to_varint( data["extraPayloadSize"]) + bytes.fromhex(data["extraPayload"]) # KeepKey firmware doesn't understand the split of version and type, so let's mimic the # old serialization format t.version |= dip2_type << 16 return t
def write_int_list_buf(data: List[int]) -> bytearray: ret_data = num_to_varint(len(data)) for item in data: ret_data += num_to_varint(item) return ret_data
def write_bytes_buf(data: ByteString) -> bytearray: return num_to_varint(len(data)) + data
def sign_tx(hw_session: HwSessionInfo, utxos_to_spend: List[wallet_common.UtxoType], tx_outputs: List[wallet_common.TxOutputType], tx_fee): client = hw_session.hw_client rawtransactions = {} decodedtransactions = {} # Each of the UTXOs will become an input in the new transaction. For each of those inputs, create # a Ledger's 'trusted input', that will be used by the the device to sign a transaction. trusted_inputs = [] # arg_inputs: list of dicts # { # 'locking_script': <Locking script of the UTXO used as an input. Used in the process of signing # transaction.>, # 'outputIndex': <index of the UTXO within the previus transaction>, # 'txid': <hash of the previus transaction>, # 'bip32_path': <BIP32 path of the HW key controlling UTXO's destination>, # 'pubkey': <Public key obtained from the HW using the bip32_path.> # 'signature' <Signature obtained as a result of processing the input. It will be used as a part of the # unlocking script.> # } # Why do we need a locking script of the previous transaction? When hashing a new transaction before creating its # signature, all placeholders for input's unlocking script has to be filled with locking script of the # corresponding UTXO. Look here for the details: # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) arg_inputs = [] # A dictionary mapping bip32 path to a pubkeys obtained from the Ledger device - used to avoid # reading it multiple times for the same bip32 path bip32_to_address = {} # read previous transactins for utxo in utxos_to_spend: if utxo.txid not in rawtransactions: tx = hw_session.hatchd_intf.getrawtransaction(utxo.txid, 1, skip_cache=False) if tx and tx.get('hex'): tx_raw = tx.get('hex') else: tx_raw = hw_session.hatchd_intf.getrawtransaction( utxo.txid, 0, skip_cache=False) if tx_raw: rawtransactions[utxo.txid] = tx_raw decodedtransactions[utxo.txid] = tx amount = 0 starting = True for idx, utxo in enumerate(utxos_to_spend): amount += utxo.satoshis raw_tx = rawtransactions.get(utxo.txid) if not raw_tx: raise Exception("Can't find raw transaction for txid: " + utxo.txid) else: raw_tx = bytearray.fromhex(raw_tx) # parse the raw transaction, so that we can extract the UTXO locking script we refer to prev_transaction = bitcoinTransaction(raw_tx) data = decodedtransactions[utxo.txid] dip2_type = data.get("type", 0) if data['version'] == 3 and dip2_type != 0: # It's a DIP2 special TX with payload if "extraPayloadSize" not in data or "extraPayload" not in data: raise ValueError("Payload data missing in DIP2 transaction") if data["extraPayloadSize"] * 2 != len(data["extraPayload"]): raise ValueError( "extra_data_len (%d) does not match calculated length (%d)" % (data["extraPayloadSize"], len(data["extraPayload"]) * 2)) prev_transaction.extra_data = hatch_utils.num_to_varint( data["extraPayloadSize"]) + bytes.fromhex(data["extraPayload"]) else: prev_transaction.extra_data = bytes() utxo_tx_index = utxo.output_index if utxo_tx_index < 0 or utxo_tx_index > len(prev_transaction.outputs): raise Exception('Incorrent value of outputIndex for UTXO %s' % str(idx)) trusted_input = client.getTrustedInput(prev_transaction, utxo_tx_index) trusted_inputs.append(trusted_input) bip32_path = utxo.bip32_path bip32_path = clean_bip32_path(bip32_path) pubkey = bip32_to_address.get(bip32_path) if not pubkey: pubkey = compress_public_key( client.getWalletPublicKey(bip32_path)['publicKey']) bip32_to_address[bip32_path] = pubkey pubkey_hash = bitcoin.bin_hash160(pubkey) # verify if the public key hash of the wallet's bip32 path is the same as specified in the UTXO locking script # if they differ, signature and public key we produce and are going to include in the unlocking script won't # match the locking script conditions - transaction will be rejected by the network pubkey_hash_from_script = extract_pkh_from_locking_script( prev_transaction.outputs[utxo_tx_index].script) if pubkey_hash != pubkey_hash_from_script: logging.error( "Error: different public key hashes for the BIP32 path %s (UTXO %s) and the UTXO locking " "script. Your signed transaction will not be validated by the network." % (bip32_path, str(idx))) arg_inputs.append({ 'locking_script': prev_transaction.outputs[utxo.output_index].script, 'pubkey': pubkey, 'bip32_path': bip32_path, 'outputIndex': utxo.output_index, 'txid': utxo.txid }) amount -= int(tx_fee) amount = int(amount) new_transaction = bitcoinTransaction( ) # new transaction object to be used for serialization at the last stage new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) for out in tx_outputs: output = bitcoinOutput() output.script = compose_tx_locking_script( out.address, hw_session.app_config.hatch_network) output.amount = int.to_bytes(out.satoshis, 8, byteorder='little') new_transaction.outputs.append(output) # join all outputs - will be used by Ledger for sigining transaction all_outputs_raw = new_transaction.serializeOutputs() # sign all inputs on Ledger and add inputs in the new_transaction object for serialization for idx, new_input in enumerate(arg_inputs): client.startUntrustedTransaction(starting, idx, trusted_inputs, new_input['locking_script']) client.finalizeInputFull(all_outputs_raw) sig = client.untrustedHashSign(new_input['bip32_path'], lockTime=0) new_input['signature'] = sig input = bitcoinInput() input.prevOut = bytearray.fromhex(new_input['txid'])[::-1] + \ int.to_bytes(new_input['outputIndex'], 4, byteorder='little') input.script = bytearray([len(sig)]) + sig + bytearray( [0x21]) + new_input['pubkey'] input.sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) new_transaction.inputs.append(input) starting = False new_transaction.lockTime = bytearray([0, 0, 0, 0]) tx_raw = bytearray(new_transaction.serialize()) return tx_raw, amount