diff --git a/python/src/trezorlib/binance.py b/python/src/trezorlib/binance.py index acb410665..9bc376df7 100644 --- a/python/src/trezorlib/binance.py +++ b/python/src/trezorlib/binance.py @@ -1,3 +1,19 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + from . import messages from .protobuf import dict_to_proto from .tools import expect, session diff --git a/python/src/trezorlib/cli/__init__.py b/python/src/trezorlib/cli/__init__.py index e69de29bb..e7e18298f 100644 --- a/python/src/trezorlib/cli/__init__.py +++ b/python/src/trezorlib/cli/__init__.py @@ -0,0 +1,27 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import click + + +class ChoiceType(click.Choice): + def __init__(self, typemap): + super(ChoiceType, self).__init__(typemap.keys()) + self.typemap = typemap + + def convert(self, value, param, ctx): + value = super(ChoiceType, self).convert(value, param, ctx) + return self.typemap[value] diff --git a/python/src/trezorlib/cli/binance.py b/python/src/trezorlib/cli/binance.py new file mode 100644 index 000000000..41b95dad7 --- /dev/null +++ b/python/src/trezorlib/cli/binance.py @@ -0,0 +1,70 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click + +from .. import binance, tools + +PATH_HELP = "BIP-32 path to key, e.g. m/44'/714'/0'/0/0" + + +@click.group(name="binance") +def cli(): + """Binance Chain commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Binance address for specified path.""" + client = connect() + address_n = tools.parse_path(address) + + return binance.get_address(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_key(connect, address, show_display): + """Get Binance public key.""" + client = connect() + address_n = tools.parse_path(address) + + return binance.get_public_key(client, address_n, show_display).hex() + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-f", + "--file", + type=click.File("r"), + required=True, + help="Transaction in JSON format", +) +@click.pass_obj +def sign_tx(connect, address, file): + """Sign Binance transaction""" + client = connect() + address_n = tools.parse_path(address) + + return binance.sign_tx(client, address_n, json.load(file)) diff --git a/python/src/trezorlib/cli/btc.py b/python/src/trezorlib/cli/btc.py new file mode 100644 index 000000000..f66becdb4 --- /dev/null +++ b/python/src/trezorlib/cli/btc.py @@ -0,0 +1,324 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import base64 +import json +import sys + +import click + +from .. import btc, coins, messages, protobuf, tools +from . import ChoiceType + +INPUT_SCRIPTS = { + "address": messages.InputScriptType.SPENDADDRESS, + "segwit": messages.InputScriptType.SPENDWITNESS, + "p2shsegwit": messages.InputScriptType.SPENDP2SHWITNESS, +} + +OUTPUT_SCRIPTS = { + "address": messages.OutputScriptType.PAYTOADDRESS, + "segwit": messages.OutputScriptType.PAYTOWITNESS, + "p2shsegwit": messages.OutputScriptType.PAYTOP2SHWITNESS, +} + +DEFAULT_COIN = "Bitcoin" + + +@click.group(name="btc") +def cli(): + """Bitcoin and Bitcoin-like coins commands.""" + + +# +# Address functions +# + + +@cli.command() +@click.option("-c", "--coin") +@click.option("-n", "--address", required=True, help="BIP-32 path") +@click.option("-t", "--script-type", type=ChoiceType(INPUT_SCRIPTS), default="address") +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, coin, address, script_type, show_display): + """Get address for specified path.""" + coin = coin or DEFAULT_COIN + address_n = tools.parse_path(address) + return btc.get_address( + connect(), coin, address_n, show_display, script_type=script_type + ) + + +@cli.command() +@click.option("-c", "--coin") +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'") +@click.option("-e", "--curve") +@click.option("-t", "--script-type", type=ChoiceType(INPUT_SCRIPTS), default="address") +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_node(connect, coin, address, curve, script_type, show_display): + """Get public node of given path.""" + coin = coin or DEFAULT_COIN + address_n = tools.parse_path(address) + result = btc.get_public_node( + connect(), + address_n, + ecdsa_curve_name=curve, + show_display=show_display, + coin_name=coin, + script_type=script_type, + ) + return { + "node": { + "depth": result.node.depth, + "fingerprint": "%08x" % result.node.fingerprint, + "child_num": result.node.child_num, + "chain_code": result.node.chain_code.hex(), + "public_key": result.node.public_key.hex(), + }, + "xpub": result.xpub, + } + + +# +# Signing functions +# + + +@cli.command() +@click.option("-c", "--coin") +@click.argument("json_file", type=click.File(), required=False) +@click.pass_obj +def sign_tx(connect, coin, json_file): + """Sign transaction.""" + client = connect() + coin = coin or DEFAULT_COIN + + if json_file is None: + return _sign_interactive(client, coin) + + data = json.load(json_file) + coin = data.get("coin_name", coin) + details = protobuf.dict_to_proto(messages.SignTx, data.get("details", {})) + inputs = [ + protobuf.dict_to_proto(messages.TxInputType, i) for i in data.get("inputs", ()) + ] + outputs = [ + protobuf.dict_to_proto(messages.TxOutputType, output) + for output in data.get("outputs", ()) + ] + prev_txes = { + bytes.fromhex(txid): protobuf.dict_to_proto(messages.TransactionType, tx) + for txid, tx in data.get("prev_txes", {}).items() + } + + _, serialized_tx = btc.sign_tx(client, coin, inputs, outputs, details, prev_txes) + + click.echo() + click.echo("Signed Transaction:") + click.echo(serialized_tx.hex()) + + +# +# Message functions +# + + +@cli.command() +@click.option("-c", "--coin") +@click.option("-n", "--address", required=True, help="BIP-32 path") +@click.option("-t", "--script-type", type=ChoiceType(INPUT_SCRIPTS), default="address") +@click.argument("message") +@click.pass_obj +def sign_message(connect, coin, address, message, script_type): + """Sign message using address of given path.""" + coin = coin or DEFAULT_COIN + address_n = tools.parse_path(address) + res = btc.sign_message(connect(), coin, address_n, message, script_type) + return { + "message": message, + "address": res.address, + "signature": base64.b64encode(res.signature).decode(), + } + + +@cli.command() +@click.option("-c", "--coin") +@click.argument("address") +@click.argument("signature") +@click.argument("message") +@click.pass_obj +def verify_message(connect, coin, address, signature, message): + """Verify message.""" + signature = base64.b64decode(signature) + coin = coin or DEFAULT_COIN + return btc.verify_message(connect(), coin, address, signature, message) + + +# +# deprecated interactive signing +# ALL BELOW is legacy code and will be dropped + + +def _default_script_type(address_n, script_types): + script_type = "address" + + if address_n is None: + pass + elif address_n[0] == tools.H_(49): + script_type = "p2shsegwit" + elif address_n[0] == tools.H_(84): + script_type = "segwit" + + return script_types[script_type] + + +def _get_inputs_interactive(coin_data, txapi): + def outpoint(s): + txid, vout = s.split(":") + return bytes.fromhex(txid), int(vout) + + inputs = [] + txes = {} + while True: + click.echo() + prev = click.prompt( + "Previous output to spend (txid:vout)", type=outpoint, default="" + ) + if not prev: + break + prev_hash, prev_index = prev + address_n = click.prompt("BIP-32 path to derive the key", type=tools.parse_path) + try: + tx = txapi[prev_hash] + txes[prev_hash] = tx + amount = tx.bin_outputs[prev_index].amount + click.echo("Prefilling input amount: {}".format(amount)) + except Exception as e: + print(e) + click.echo("Failed to fetch transation. This might bite you later.") + amount = click.prompt("Input amount (satoshis)", type=int, default=0) + + sequence = click.prompt( + "Sequence Number to use (RBF opt-in enabled by default)", + type=int, + default=0xFFFFFFFD, + ) + script_type = click.prompt( + "Input type", + type=ChoiceType(INPUT_SCRIPTS), + default=_default_script_type(address_n, INPUT_SCRIPTS), + ) + + new_input = messages.TxInputType( + address_n=address_n, + prev_hash=prev_hash, + prev_index=prev_index, + amount=amount, + script_type=script_type, + sequence=sequence, + ) + if coin_data["bip115"]: + prev_output = txapi.get_tx(prev_hash.hex()).bin_outputs[prev_index] + new_input.prev_block_hash_bip115 = prev_output.block_hash + new_input.prev_block_height_bip115 = prev_output.block_height + + inputs.append(new_input) + + return inputs, txes + + +def _get_outputs_interactive(): + outputs = [] + while True: + click.echo() + address = click.prompt("Output address (for non-change output)", default="") + if address: + address_n = None + else: + address = None + address_n = click.prompt( + "BIP-32 path (for change output)", type=tools.parse_path, default="" + ) + if not address_n: + break + amount = click.prompt("Amount to spend (satoshis)", type=int) + script_type = click.prompt( + "Output type", + type=ChoiceType(OUTPUT_SCRIPTS), + default=_default_script_type(address_n, OUTPUT_SCRIPTS), + ) + outputs.append( + messages.TxOutputType( + address_n=address_n, + address=address, + amount=amount, + script_type=script_type, + ) + ) + + return outputs + + +def _sign_interactive(client, coin): + click.echo("Warning: interactive sign-tx mode is deprecated.", err=True) + click.echo( + "Instead, you should format your transaction data as JSON and " + "supply the file as an argument to sign-tx", + err=True, + ) + if coin in coins.tx_api: + coin_data = coins.by_name[coin] + txapi = coins.tx_api[coin] + else: + click.echo('Coin "%s" is not recognized.' % coin, err=True) + click.echo( + "Supported coin types: %s" % ", ".join(coins.tx_api.keys()), err=True + ) + sys.exit(1) + + inputs, txes = _get_inputs_interactive(coin_data, txapi) + outputs = _get_outputs_interactive() + + if coin_data["bip115"]: + current_block_height = txapi.current_height() + # Zencash recommendation for the better protection + block_height = current_block_height - 300 + block_hash = txapi.get_block_hash(block_height) + # Blockhash passed in reverse order + block_hash = block_hash[::-1] + + for output in outputs: + output.block_hash_bip115 = block_hash + output.block_height_bip115 = block_height + + signtx = messages.SignTx() + signtx.version = click.prompt("Transaction version", type=int, default=2) + signtx.lock_time = click.prompt("Transaction locktime", type=int, default=0) + if coin == "Capricoin": + signtx.timestamp = click.prompt("Transaction timestamp", type=int) + + _, serialized_tx = btc.sign_tx( + client, coin, inputs, outputs, details=signtx, prev_txes=txes + ) + + click.echo() + click.echo("Signed Transaction:") + click.echo(serialized_tx.hex()) + click.echo() + click.echo("Use the following form to broadcast it to the network:") + click.echo(txapi.pushtx_url) diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py new file mode 100644 index 000000000..73987fd60 --- /dev/null +++ b/python/src/trezorlib/cli/cardano.py @@ -0,0 +1,79 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click + +from .. import cardano, tools + +PATH_HELP = "BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" + + +@click.group(name="cardano") +def cli(): + """Cardano commands.""" + + +@cli.command() +@click.option( + "-f", + "--file", + type=click.File("r"), + required=True, + help="Transaction in JSON format", +) +@click.option("-N", "--network", type=int, default=1) +@click.pass_obj +def sign_tx(connect, file, network): + """Sign Cardano transaction.""" + client = connect() + + transaction = json.load(file) + + inputs = [cardano.create_input(input) for input in transaction["inputs"]] + outputs = [cardano.create_output(output) for output in transaction["outputs"]] + transactions = transaction["transactions"] + + signed_transaction = cardano.sign_tx(client, inputs, outputs, transactions, network) + + return { + "tx_hash": signed_transaction.tx_hash.hex(), + "tx_body": signed_transaction.tx_body.hex(), + } + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Cardano address.""" + client = connect() + address_n = tools.parse_path(address) + + return cardano.get_address(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.pass_obj +def get_public_key(connect, address): + """Get Cardano public key.""" + client = connect() + address_n = tools.parse_path(address) + + return cardano.get_public_key(client, address_n) diff --git a/python/src/trezorlib/cli/cosi.py b/python/src/trezorlib/cli/cosi.py new file mode 100644 index 000000000..ce57a261e --- /dev/null +++ b/python/src/trezorlib/cli/cosi.py @@ -0,0 +1,56 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import click + +from .. import cosi, tools + +PATH_HELP = "BIP-32 path, e.g. m/44'/0'/0'/0/0" + + +@click.group(name="cosi") +def cli(): + """CoSi (Cothority / collective signing) commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.argument("data") +@click.pass_obj +def commit(connect, address, data): + """Ask device to commit to CoSi signing.""" + client = connect() + address_n = tools.parse_path(address) + return cosi.commit(client, address_n, bytes.fromhex(data)) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.argument("data") +@click.argument("global_commitment") +@click.argument("global_pubkey") +@click.pass_obj +def sign(connect, address, data, global_commitment, global_pubkey): + """Ask device to sign using CoSi.""" + client = connect() + address_n = tools.parse_path(address) + return cosi.sign( + client, + address_n, + bytes.fromhex(data), + bytes.fromhex(global_commitment), + bytes.fromhex(global_pubkey), + ) diff --git a/python/src/trezorlib/cli/crypto.py b/python/src/trezorlib/cli/crypto.py new file mode 100644 index 000000000..a9c6190eb --- /dev/null +++ b/python/src/trezorlib/cli/crypto.py @@ -0,0 +1,57 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import click + +from .. import misc, tools + + +@click.group(name="crypto") +def cli(): + """Miscellaneous cryptography features.""" + + +@cli.command() +@click.argument("size", type=int) +@click.pass_obj +def get_entropy(connect, size): + """Get random bytes from device.""" + return misc.get_entropy(connect(), size).hex() + + +@cli.command() +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/10016'/0") +@click.argument("key") +@click.argument("value") +@click.pass_obj +def encrypt_keyvalue(connect, address, key, value): + """Encrypt value by given key and path.""" + client = connect() + address_n = tools.parse_path(address) + res = misc.encrypt_keyvalue(client, address_n, key, value.encode()) + return res.hex() + + +@cli.command() +@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/10016'/0") +@click.argument("key") +@click.argument("value") +@click.pass_obj +def decrypt_keyvalue(connect, address, key, value): + """Decrypt value by given key and path.""" + client = connect() + address_n = tools.parse_path(address) + return misc.decrypt_keyvalue(client, address_n, key, bytes.fromhex(value)) diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py new file mode 100644 index 000000000..8f9557c86 --- /dev/null +++ b/python/src/trezorlib/cli/device.py @@ -0,0 +1,262 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import sys + +import click + +from .. import debuglink, device, exceptions, messages, ui +from . import ChoiceType + +RECOVERY_TYPE = { + "scrambled": messages.RecoveryDeviceType.ScrambledWords, + "matrix": messages.RecoveryDeviceType.Matrix, +} + +BACKUP_TYPE = { + "single": messages.BackupType.Bip39, + "shamir": messages.BackupType.Slip39_Basic, + "advanced": messages.BackupType.Slip39_Advanced, +} + +SD_PROTECT_OPERATIONS = { + "enable": messages.SdProtectOperationType.ENABLE, + "disable": messages.SdProtectOperationType.DISABLE, + "refresh": messages.SdProtectOperationType.REFRESH, +} + + +@click.group(name="device") +def cli(): + """Device management commands - setup, recover seed, wipe, etc.""" + + +@cli.command() +@click.pass_obj +def self_test(connect): + """Perform a self-test.""" + return debuglink.self_test(connect()) + + +@cli.command() +@click.option( + "-b", + "--bootloader", + help="Wipe device in bootloader mode. This also erases the firmware.", + is_flag=True, +) +@click.pass_obj +def wipe(connect, bootloader): + """Reset device to factory defaults and remove all private data.""" + client = connect() + if bootloader: + if not client.features.bootloader_mode: + click.echo("Please switch your device to bootloader mode.") + sys.exit(1) + else: + click.echo("Wiping user data and firmware!") + else: + if client.features.bootloader_mode: + click.echo( + "Your device is in bootloader mode. This operation would also erase firmware." + ) + click.echo( + 'Specify "--bootloader" if that is what you want, or disconnect and reconnect device in normal mode.' + ) + click.echo("Aborting.") + sys.exit(1) + else: + click.echo("Wiping user data!") + + try: + return device.wipe(connect()) + except exceptions.TrezorFailure as e: + click.echo("Action failed: {} {}".format(*e.args)) + sys.exit(3) + + +@cli.command() +@click.option("-m", "--mnemonic", multiple=True) +@click.option("-p", "--pin", default="") +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.option("-l", "--label", default="") +@click.option("-i", "--ignore-checksum", is_flag=True) +@click.option("-s", "--slip0014", is_flag=True) +@click.option("-b", "--needs-backup", is_flag=True) +@click.option("-n", "--no-backup", is_flag=True) +@click.pass_obj +def load( + connect, + mnemonic, + pin, + passphrase_protection, + label, + ignore_checksum, + slip0014, + needs_backup, + no_backup, +): + """Upload seed and custom configuration to the device. + + This functionality is only available in debug mode. + """ + if slip0014 and mnemonic: + raise click.ClickException("Cannot use -s and -m together.") + + client = connect() + + if slip0014: + mnemonic = [" ".join(["all"] * 12)] + if not label: + label = "SLIP-0014" + + return debuglink.load_device( + client, + mnemonic=list(mnemonic), + pin=pin, + passphrase_protection=passphrase_protection, + label=label, + language="english", + skip_checksum=ignore_checksum, + needs_backup=needs_backup, + no_backup=no_backup, + ) + + +@cli.command() +@click.option("-w", "--words", type=click.Choice(["12", "18", "24"]), default="24") +@click.option("-e", "--expand", is_flag=True) +@click.option("-p", "--pin-protection", is_flag=True) +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.option("-l", "--label") +@click.option( + "-t", "--type", "rec_type", type=ChoiceType(RECOVERY_TYPE), default="scrambled" +) +@click.option("-d", "--dry-run", is_flag=True) +@click.pass_obj +def recover( + connect, + words, + expand, + pin_protection, + passphrase_protection, + label, + rec_type, + dry_run, +): + """Start safe recovery workflow.""" + if rec_type == messages.RecoveryDeviceType.ScrambledWords: + input_callback = ui.mnemonic_words(expand) + else: + input_callback = ui.matrix_words + click.echo(ui.RECOVERY_MATRIX_DESCRIPTION) + + return device.recover( + connect(), + word_count=int(words), + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label, + language="english", + input_callback=input_callback, + type=rec_type, + dry_run=dry_run, + ) + + +@cli.command() +@click.option("-e", "--show-entropy", is_flag=True) +@click.option("-t", "--strength", type=click.Choice(["128", "192", "256"])) +@click.option("-r", "--passphrase-protection", is_flag=True) +@click.option("-p", "--pin-protection", is_flag=True) +@click.option("-l", "--label") +@click.option("-u", "--u2f-counter", default=0) +@click.option("-s", "--skip-backup", is_flag=True) +@click.option("-n", "--no-backup", is_flag=True) +@click.option("-b", "--backup-type", type=ChoiceType(BACKUP_TYPE), default="single") +@click.pass_obj +def setup( + connect, + show_entropy, + strength, + passphrase_protection, + pin_protection, + label, + u2f_counter, + skip_backup, + no_backup, + backup_type, +): + """Perform device setup and generate new seed.""" + if strength: + strength = int(strength) + + client = connect() + if ( + backup_type == messages.BackupType.Slip39_Basic + and messages.Capability.Shamir not in client.features.capabilities + ) or ( + backup_type == messages.BackupType.Slip39_Advanced + and messages.Capability.ShamirGroups not in client.features.capabilities + ): + click.echo( + "WARNING: Your Trezor device does not indicate support for the requested\n" + "backup type. Traditional single-seed backup may be generated instead." + ) + + return device.reset( + client, + display_random=show_entropy, + strength=strength, + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label, + language="english", + u2f_counter=u2f_counter, + skip_backup=skip_backup, + no_backup=no_backup, + backup_type=backup_type, + ) + + +@cli.command() +@click.pass_obj +def backup(connect): + """Perform device seed backup.""" + return device.backup(connect()) + + +@cli.command() +@click.argument("operation", type=ChoiceType(SD_PROTECT_OPERATIONS)) +@click.pass_obj +def sd_protect(connect, operation): + """Secure the device with SD card protection. + + When SD card protection is enabled, a randomly generated secret is stored + on the SD card. During every PIN checking and unlocking operation this + secret is combined with the entered PIN value to decrypt data stored on + the device. The SD card will thus be needed every time you unlock the + device. The options are: + + \b + enable - Generate SD card secret and use it to protect the PIN and storage. + disable - Remove SD card secret protection. + refresh - Replace the current SD card secret with a new one. + """ + client = connect() + if client.features.model == "1": + raise click.BadUsage("Trezor One does not support SD card protection.") + return device.sd_protect(client, operation) diff --git a/python/src/trezorlib/cli/eos.py b/python/src/trezorlib/cli/eos.py new file mode 100644 index 000000000..f39ca8aa7 --- /dev/null +++ b/python/src/trezorlib/cli/eos.py @@ -0,0 +1,60 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click + +from .. import eos, tools + +PATH_HELP = "BIP-32 path, e.g. m/44'/194'/0'/0/0" + + +@click.group(name="eos") +def cli(): + """EOS commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_key(connect, address, show_display): + """Get Eos public key in base58 encoding.""" + client = connect() + address_n = tools.parse_path(address) + res = eos.get_public_key(client, address_n, show_display) + return "WIF: {}\nRaw: {}".format(res.wif_public_key, res.raw_public_key.hex()) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-f", + "--file", + type=click.File("r"), + required=True, + help="Transaction in JSON format", +) +@click.pass_obj +def sign_transaction(connect, address, file): + """Sign EOS transaction.""" + client = connect() + + tx_json = json.load(file) + + address_n = tools.parse_path(address) + return eos.sign_tx(client, address_n, tx_json["transaction"], tx_json["chain_id"]) diff --git a/python/src/trezorlib/cli/ethereum.py b/python/src/trezorlib/cli/ethereum.py new file mode 100644 index 000000000..5e4016586 --- /dev/null +++ b/python/src/trezorlib/cli/ethereum.py @@ -0,0 +1,321 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import re +import sys +from decimal import Decimal + +import click + +from .. import ethereum, tools + +try: + import rlp + import web3 + + HAVE_SIGN_TX = True +except Exception: + HAVE_SIGN_TX = False + + +PATH_HELP = "BIP-32 path, e.g. m/44'/60'/0'/0/0" + +# fmt: off +ETHER_UNITS = { + 'wei': 1, + 'kwei': 1000, + 'babbage': 1000, + 'femtoether': 1000, + 'mwei': 1000000, + 'lovelace': 1000000, + 'picoether': 1000000, + 'gwei': 1000000000, + 'shannon': 1000000000, + 'nanoether': 1000000000, + 'nano': 1000000000, + 'szabo': 1000000000000, + 'microether': 1000000000000, + 'micro': 1000000000000, + 'finney': 1000000000000000, + 'milliether': 1000000000000000, + 'milli': 1000000000000000, + 'ether': 1000000000000000000, + 'eth': 1000000000000000000, +} +# fmt: on + + +def _amount_to_int(ctx, param, value): + if value is None: + return None + if value.isdigit(): + return int(value) + try: + number, unit = re.match(r"^(\d+(?:.\d+)?)([a-z]+)", value).groups() + scale = ETHER_UNITS[unit] + decoded_number = Decimal(number) + return int(decoded_number * scale) + + except Exception: + raise click.BadParameter("Amount not understood") + + +def _list_units(ctx, param, value): + if not value or ctx.resilient_parsing: + return + maxlen = max(len(k) for k in ETHER_UNITS.keys()) + 1 + for unit, scale in ETHER_UNITS.items(): + click.echo("{:{maxlen}}: {}".format(unit, scale, maxlen=maxlen)) + ctx.exit() + + +def _decode_hex(value): + if value.startswith("0x") or value.startswith("0X"): + return bytes.fromhex(value[2:]) + else: + return bytes.fromhex(value) + + +def _erc20_contract(w3, token_address, to_address, amount): + min_abi = [ + { + "name": "transfer", + "type": "function", + "constant": False, + "inputs": [ + {"name": "_to", "type": "address"}, + {"name": "_value", "type": "uint256"}, + ], + "outputs": [{"name": "", "type": "bool"}], + } + ] + contract = w3.eth.contract(address=token_address, abi=min_abi) + return contract.encodeABI("transfer", [to_address, amount]) + + +##################### +# +# commands start here + + +@click.group(name="ethereum") +def cli(): + """Ethereum commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Ethereum address in hex encoding.""" + client = connect() + address_n = tools.parse_path(address) + return ethereum.get_address(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_node(connect, address, show_display): + """Get Ethereum public node of given path.""" + client = connect() + address_n = tools.parse_path(address) + result = ethereum.get_public_node(client, address_n, show_display=show_display) + return { + "node": { + "depth": result.node.depth, + "fingerprint": "%08x" % result.node.fingerprint, + "child_num": result.node.child_num, + "chain_code": result.node.chain_code.hex(), + "public_key": result.node.public_key.hex(), + }, + "xpub": result.xpub, + } + + +@cli.command() +@click.option( + "-c", "--chain-id", type=int, default=1, help="EIP-155 chain id (replay protection)" +) +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-g", "--gas-limit", type=int, help="Gas limit (required for offline signing)" +) +@click.option( + "-t", + "--gas-price", + help="Gas price (required for offline signing)", + callback=_amount_to_int, +) +@click.option( + "-i", "--nonce", type=int, help="Transaction counter (required for offline signing)" +) +@click.option("-d", "--data", help="Data as hex string, e.g. 0x12345678") +@click.option("-p", "--publish", is_flag=True, help="Publish transaction via RPC") +@click.option("-x", "--tx-type", type=int, help="TX type (used only for Wanchain)") +@click.option("-t", "--token", help="ERC20 token address") +@click.option( + "--list-units", + is_flag=True, + help="List known currency units and exit.", + is_eager=True, + callback=_list_units, + expose_value=False, +) +@click.argument("to_address") +@click.argument("amount", callback=_amount_to_int) +@click.pass_obj +def sign_tx( + connect, + chain_id, + address, + amount, + gas_limit, + gas_price, + nonce, + data, + publish, + to_address, + tx_type, + token, +): + """Sign (and optionally publish) Ethereum transaction. + + Use TO_ADDRESS as destination address, or set to "" for contract creation. + + Specify a contract address with the --token option to send an ERC20 token. + + You can specify AMOUNT and gas price either as a number of wei, + or you can use a unit suffix. + + Use the --list-units option to show all known currency units. + ERC20 token amounts are specified in eth/wei, custom units are not supported. + + If any of gas price, gas limit and nonce is not specified, this command will + try to connect to an ethereum node and auto-fill these values. You can configure + the connection with WEB3_PROVIDER_URI environment variable. + """ + if not HAVE_SIGN_TX: + click.echo("Ethereum requirements not installed.") + click.echo("Please run:") + click.echo() + click.echo(" pip install web3 rlp") + sys.exit(1) + + w3 = web3.Web3() + if ( + any(x is None for x in (gas_price, gas_limit, nonce)) + or publish + and not w3.isConnected() + ): + click.echo("Failed to connect to Ethereum node.") + click.echo( + "If you want to sign offline, make sure you provide --gas-price, " + "--gas-limit and --nonce arguments" + ) + sys.exit(1) + + if data is not None and token is not None: + click.echo("Can't send tokens and custom data at the same time") + sys.exit(1) + + client = connect() + address_n = tools.parse_path(address) + from_address = ethereum.get_address(client, address_n) + + if token: + data = _erc20_contract(w3, token, to_address, amount) + to_address = token + amount = 0 + + if data: + data = _decode_hex(data) + else: + data = b"" + + if gas_price is None: + gas_price = w3.eth.gasPrice + + if gas_limit is None: + gas_limit = w3.eth.estimateGas( + { + "to": to_address, + "from": from_address, + "value": amount, + "data": "0x%s" % data.hex(), + } + ) + + if nonce is None: + nonce = w3.eth.getTransactionCount(from_address) + + sig = ethereum.sign_tx( + client, + n=address_n, + tx_type=tx_type, + nonce=nonce, + gas_price=gas_price, + gas_limit=gas_limit, + to=to_address, + value=amount, + data=data, + chain_id=chain_id, + ) + + to = _decode_hex(to_address) + if tx_type is None: + transaction = rlp.encode((nonce, gas_price, gas_limit, to, amount, data) + sig) + else: + transaction = rlp.encode( + (tx_type, nonce, gas_price, gas_limit, to, amount, data) + sig + ) + tx_hex = "0x%s" % transaction.hex() + + if publish: + tx_hash = w3.eth.sendRawTransaction(tx_hex).hex() + return "Transaction published with ID: %s" % tx_hash + else: + return "Signed raw transaction:\n%s" % tx_hex + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.argument("message") +@click.pass_obj +def sign_message(connect, address, message): + """Sign message with Ethereum address.""" + client = connect() + address_n = tools.parse_path(address) + ret = ethereum.sign_message(client, address_n, message) + output = { + "message": message, + "address": ret.address, + "signature": "0x%s" % ret.signature.hex(), + } + return output + + +@cli.command() +@click.argument("address") +@click.argument("signature") +@click.argument("message") +@click.pass_obj +def verify_message(connect, address, signature, message): + """Verify message signed with Ethereum address.""" + signature = _decode_hex(signature) + return ethereum.verify_message(connect(), address, signature, message) diff --git a/python/src/trezorlib/cli/firmware.py b/python/src/trezorlib/cli/firmware.py new file mode 100644 index 000000000..28046763e --- /dev/null +++ b/python/src/trezorlib/cli/firmware.py @@ -0,0 +1,272 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import sys + +import click +import requests + +from .. import exceptions, firmware + +ALLOWED_FIRMWARE_FORMATS = { + 1: (firmware.FirmwareFormat.TREZOR_ONE, firmware.FirmwareFormat.TREZOR_ONE_V2), + 2: (firmware.FirmwareFormat.TREZOR_T,), +} + + +def _print_version(version): + vstr = "Firmware version {major}.{minor}.{patch} build {build}".format(**version) + click.echo(vstr) + + +def validate_firmware(version, fw, expected_fingerprint=None): + if version == firmware.FirmwareFormat.TREZOR_ONE: + if fw.embedded_onev2: + click.echo("Trezor One firmware with embedded v2 image (1.8.0 or later)") + _print_version(fw.embedded_onev2.firmware_header.version) + else: + click.echo("Trezor One firmware image.") + elif version == firmware.FirmwareFormat.TREZOR_ONE_V2: + click.echo("Trezor One v2 firmware (1.8.0 or later)") + _print_version(fw.firmware_header.version) + elif version == firmware.FirmwareFormat.TREZOR_T: + click.echo("Trezor T firmware image.") + vendor = fw.vendor_header.vendor_string + vendor_version = "{major}.{minor}".format(**fw.vendor_header.version) + click.echo("Vendor header from {}, version {}".format(vendor, vendor_version)) + _print_version(fw.firmware_header.version) + + try: + firmware.validate(version, fw, allow_unsigned=False) + click.echo("Signatures are valid.") + except firmware.Unsigned: + if not click.confirm("No signatures found. Continue?", default=False): + sys.exit(1) + try: + firmware.validate(version, fw, allow_unsigned=True) + click.echo("Unsigned firmware looking OK.") + except firmware.FirmwareIntegrityError as e: + click.echo(e) + click.echo("Firmware validation failed, aborting.") + sys.exit(4) + except firmware.FirmwareIntegrityError as e: + click.echo(e) + click.echo("Firmware validation failed, aborting.") + sys.exit(4) + + fingerprint = firmware.digest(version, fw).hex() + click.echo("Firmware fingerprint: {}".format(fingerprint)) + if expected_fingerprint and fingerprint != expected_fingerprint: + click.echo("Expected fingerprint: {}".format(expected_fingerprint)) + click.echo("Fingerprints do not match, aborting.") + sys.exit(5) + + +def find_best_firmware_version( + bootloader_version, requested_version=None, beta=False, bitcoin_only=False +): + if beta: + url = "https://beta-wallet.trezor.io/data/firmware/{}/releases.json" + else: + url = "https://wallet.trezor.io/data/firmware/{}/releases.json" + releases = requests.get(url.format(bootloader_version[0])).json() + if not releases: + raise click.ClickException("Failed to get list of releases") + + if bitcoin_only: + releases = [r for r in releases if "url_bitcoinonly" in r] + releases.sort(key=lambda r: r["version"], reverse=True) + + def version_str(version): + return ".".join(map(str, version)) + + want_version = requested_version + + if want_version is None: + want_version = releases[0]["version"] + click.echo("Best available version: {}".format(version_str(want_version))) + + confirm_different_version = False + while True: + want_version_str = version_str(want_version) + try: + release = next(r for r in releases if r["version"] == want_version) + except StopIteration: + click.echo("Version {} not found.".format(want_version_str)) + sys.exit(1) + + if ( + "min_bootloader_version" in release + and release["min_bootloader_version"] > bootloader_version + ): + need_version_str = version_str(release["min_firmware_version"]) + click.echo( + "Version {} is required before upgrading to {}.".format( + need_version_str, want_version_str + ) + ) + want_version = release["min_firmware_version"] + confirm_different_version = True + else: + break + + if confirm_different_version: + installing_different = "Installing version {} instead.".format(want_version_str) + if requested_version is None: + click.echo(installing_different) + else: + ok = click.confirm(installing_different + " Continue?", default=True) + if not ok: + sys.exit(1) + + if bitcoin_only: + url = release["url_bitcoinonly"] + fingerprint = release["fingerprint_bitcoinonly"] + else: + url = release["url"] + fingerprint = release["fingerprint"] + if beta: + url = "https://beta-wallet.trezor.io/" + url + else: + url = "https://wallet.trezor.io/" + url + + return url, fingerprint + + +@click.command() +# fmt: off +@click.option("-f", "--filename") +@click.option("-u", "--url") +@click.option("-v", "--version") +@click.option("-s", "--skip-check", is_flag=True, help="Do not validate firmware integrity") +@click.option("-n", "--dry-run", is_flag=True, help="Perform all steps but do not actually upload the firmware") +@click.option("--beta", is_flag=True, help="Use firmware from BETA wallet") +@click.option("--bitcoin-only", is_flag=True, help="Use bitcoin-only firmware (if possible)") +@click.option("--raw", is_flag=True, help="Push raw data to Trezor") +@click.option("--fingerprint", help="Expected firmware fingerprint in hex") +@click.option("--skip-vendor-header", help="Skip vendor header validation on Trezor T") +# fmt: on +@click.pass_obj +def firmware_update( + connect, + filename, + url, + version, + skip_check, + fingerprint, + skip_vendor_header, + raw, + dry_run, + beta, + bitcoin_only, +): + """Upload new firmware to device. + + Device must be in bootloader mode. + + You can specify a filename or URL from which the firmware can be downloaded. + You can also explicitly specify a firmware version that you want. + Otherwise, trezorctl will attempt to find latest available version + from wallet.trezor.io. + + If you provide a fingerprint via the --fingerprint option, it will be checked + against downloaded firmware fingerprint. Otherwise fingerprint is checked + against wallet.trezor.io information, if available. + + If you are customizing Model T bootloader and providing your own vendor header, + you can use --skip-vendor-header to ignore vendor header signatures. + """ + if sum(bool(x) for x in (filename, url, version)) > 1: + click.echo("You can use only one of: filename, url, version.") + sys.exit(1) + + client = connect() + if not dry_run and not client.features.bootloader_mode: + click.echo("Please switch your device to bootloader mode.") + sys.exit(1) + + f = client.features + bootloader_onev2 = f.major_version == 1 and f.minor_version >= 8 + + if filename: + data = open(filename, "rb").read() + else: + if not url: + bootloader_version = [f.major_version, f.minor_version, f.patch_version] + version_list = [int(x) for x in version.split(".")] if version else None + url, fp = find_best_firmware_version( + bootloader_version, version_list, beta, bitcoin_only + ) + if not fingerprint: + fingerprint = fp + + try: + click.echo("Downloading from {}".format(url)) + r = requests.get(url) + r.raise_for_status() + except requests.exceptions.HTTPError as err: + click.echo("Error downloading file: {}".format(err)) + sys.exit(3) + + data = r.content + + if not raw and not skip_check: + try: + version, fw = firmware.parse(data) + except Exception as e: + click.echo(e) + sys.exit(2) + + validate_firmware(version, fw, fingerprint) + if ( + bootloader_onev2 + and version == firmware.FirmwareFormat.TREZOR_ONE + and not fw.embedded_onev2 + ): + click.echo("Firmware is too old for your device. Aborting.") + sys.exit(3) + elif not bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE_V2: + click.echo("You need to upgrade to bootloader 1.8.0 first.") + sys.exit(3) + + if f.major_version not in ALLOWED_FIRMWARE_FORMATS: + click.echo("trezorctl doesn't know your device version. Aborting.") + sys.exit(3) + elif version not in ALLOWED_FIRMWARE_FORMATS[f.major_version]: + click.echo("Firmware does not match your device, aborting.") + sys.exit(3) + + if not raw: + # special handling for embedded-OneV2 format: + # for bootloader < 1.8, keep the embedding + # for bootloader 1.8.0 and up, strip the old OneV1 header + if bootloader_onev2 and data[:4] == b"TRZR" and data[256 : 256 + 4] == b"TRZF": + click.echo("Extracting embedded firmware image (fingerprint may change).") + data = data[256:] + + if dry_run: + click.echo("Dry run. Not uploading firmware to device.") + else: + try: + if f.major_version == 1 and f.firmware_present is not False: + # Trezor One does not send ButtonRequest + click.echo("Please confirm the action on your Trezor device") + return firmware.update(client, data) + except exceptions.Cancelled: + click.echo("Update aborted on device.") + except exceptions.TrezorException as e: + click.echo("Update failed: {}".format(e)) + sys.exit(3) diff --git a/python/src/trezorlib/cli/lisk.py b/python/src/trezorlib/cli/lisk.py new file mode 100644 index 000000000..9c584b1c3 --- /dev/null +++ b/python/src/trezorlib/cli/lisk.py @@ -0,0 +1,99 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click + +from .. import lisk, tools + +PATH_HELP = "BIP-32 path, e.g. m/44'/134'/0'/0'" + + +@click.group(name="lisk") +def cli(): + """Lisk commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Lisk address for specified path.""" + client = connect() + address_n = tools.parse_path(address) + return lisk.get_address(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_key(connect, address, show_display): + """Get Lisk public key for specified path.""" + client = connect() + address_n = tools.parse_path(address) + res = lisk.get_public_key(client, address_n, show_display) + output = {"public_key": res.public_key.hex()} + return output + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-f", "--file", type=click.File("r"), default="-", help="Transaction in JSON format" +) +# @click.option('-b', '--broadcast', help='Broadcast Lisk transaction') +@click.pass_obj +def sign_tx(connect, address, file): + """Sign Lisk transaction.""" + client = connect() + address_n = tools.parse_path(address) + transaction = lisk.sign_tx(client, address_n, json.load(file)) + + payload = {"signature": transaction.signature.hex()} + + return payload + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.argument("message") +@click.pass_obj +def sign_message(connect, address, message): + """Sign message with Lisk address.""" + client = connect() + address_n = client.expand_path(address) + res = lisk.sign_message(client, address_n, message) + output = { + "message": message, + "public_key": res.public_key.hex(), + "signature": res.signature.hex(), + } + return output + + +@cli.command() +@click.argument("pubkey") +@click.argument("signature") +@click.argument("message") +@click.pass_obj +def verify_message(connect, pubkey, signature, message): + """Verify message signed with Lisk address.""" + signature = bytes.fromhex(signature) + pubkey = bytes.fromhex(pubkey) + return lisk.verify_message(connect(), pubkey, signature, message) diff --git a/python/src/trezorlib/cli/monero.py b/python/src/trezorlib/cli/monero.py new file mode 100644 index 000000000..f844d11d9 --- /dev/null +++ b/python/src/trezorlib/cli/monero.py @@ -0,0 +1,57 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import click + +from .. import monero, tools + +PATH_HELP = "BIP-32 path, e.g. m/44'/128'/0'" + + +@click.group(name="monero") +def cli(): + """Monero commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.option( + "-t", "--network-type", type=click.Choice(["0", "1", "2", "3"]), default="0" +) +@click.pass_obj +def get_address(connect, address, show_display, network_type): + """Get Monero address for specified path.""" + client = connect() + address_n = tools.parse_path(address) + network_type = int(network_type) + return monero.get_address(client, address_n, show_display, network_type) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-t", "--network-type", type=click.Choice(["0", "1", "2", "3"]), default="0" +) +@click.pass_obj +def get_watch_key(connect, address, network_type): + """Get Monero watch key for specified path.""" + client = connect() + address_n = tools.parse_path(address) + network_type = int(network_type) + res = monero.get_watch_key(client, address_n, network_type) + output = {"address": res.address.decode(), "watch_key": res.watch_key.hex()} + return output diff --git a/python/src/trezorlib/cli/nem.py b/python/src/trezorlib/cli/nem.py new file mode 100644 index 000000000..0930bf280 --- /dev/null +++ b/python/src/trezorlib/cli/nem.py @@ -0,0 +1,68 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click +import requests + +from .. import nem, tools + +PATH_HELP = "BIP-32 path, e.g. m/44'/134'/0'/0'" + + +@click.group(name="nem") +def cli(): + """NEM commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-N", "--network", type=int, default=0x68) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, network, show_display): + """Get NEM address for specified path.""" + client = connect() + address_n = tools.parse_path(address) + return nem.get_address(client, address_n, network, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-f", + "--file", + type=click.File("r"), + default="-", + help="Transaction in NIS (RequestPrepareAnnounce) format", +) +@click.option("-b", "--broadcast", help="NIS to announce transaction to") +@click.pass_obj +def sign_tx(connect, address, file, broadcast): + """Sign (and optionally broadcast) NEM transaction.""" + client = connect() + address_n = tools.parse_path(address) + transaction = nem.sign_tx(client, address_n, json.load(file)) + + payload = {"data": transaction.data.hex(), "signature": transaction.signature.hex()} + + if broadcast: + return requests.post( + "{}/transaction/announce".format(broadcast), json=payload + ).json() + else: + return payload diff --git a/python/src/trezorlib/cli/ripple.py b/python/src/trezorlib/cli/ripple.py new file mode 100644 index 000000000..9a9acbc4e --- /dev/null +++ b/python/src/trezorlib/cli/ripple.py @@ -0,0 +1,59 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click + +from .. import ripple, tools + +PATH_HELP = "BIP-32 path to key, e.g. m/44'/144'/0'/0/0" + + +@click.group(name="ripple") +def cli(): + """Ripple commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Ripple address""" + client = connect() + address_n = tools.parse_path(address) + return ripple.get_address(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-f", "--file", type=click.File("r"), default="-", help="Transaction in JSON format" +) +@click.pass_obj +def sign_tx(connect, address, file): + """Sign Ripple transaction""" + client = connect() + address_n = tools.parse_path(address) + msg = ripple.create_sign_tx_msg(json.load(file)) + + result = ripple.sign_tx(client, address_n, msg) + click.echo("Signature:") + click.echo(result.signature.hex()) + click.echo() + click.echo("Serialized tx including the signature:") + click.echo(result.serialized_tx.hex()) diff --git a/python/src/trezorlib/cli/settings.py b/python/src/trezorlib/cli/settings.py new file mode 100644 index 000000000..bdc5d598b --- /dev/null +++ b/python/src/trezorlib/cli/settings.py @@ -0,0 +1,161 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import click + +from .. import device, messages +from . import ChoiceType + +PASSPHRASE_SOURCE = { + "ask": messages.PassphraseSourceType.ASK, + "device": messages.PassphraseSourceType.DEVICE, + "host": messages.PassphraseSourceType.HOST, +} + +ROTATION = {"north": 0, "east": 90, "south": 180, "west": 270} + + +@click.group(name="set") +def cli(): + """Device settings.""" + + +@cli.command() +@click.option("-r", "--remove", is_flag=True) +@click.pass_obj +def pin(connect, remove): + """Set, change or remove PIN.""" + return device.change_pin(connect(), remove) + + +@cli.command() +# keep the deprecated -l/--label option, make it do nothing +@click.option("-l", "--label", "_ignore", is_flag=True, hidden=True, expose_value=False) +@click.argument("label") +@click.pass_obj +def label(connect, label): + """Set new device label.""" + return device.apply_settings(connect(), label=label) + + +@cli.command() +@click.argument("rotation", type=ChoiceType(ROTATION)) +@click.pass_obj +def display_rotation(connect, rotation): + """Set display rotation. + + Configure display rotation for Trezor Model T. The options are + north, east, south or west. + """ + return device.apply_settings(connect(), display_rotation=rotation) + + +@cli.command() +@click.argument("delay", type=str) +@click.pass_obj +def auto_lock_delay(connect, delay): + """Set auto-lock delay (in seconds).""" + value, unit = delay[:-1], delay[-1:] + units = {"s": 1, "m": 60, "h": 3600} + if unit in units: + seconds = float(value) * units[unit] + else: + seconds = float(delay) # assume seconds if no unit is specified + return device.apply_settings(connect(), auto_lock_delay_ms=int(seconds * 1000)) + + +@cli.command() +@click.argument("flags") +@click.pass_obj +def flags(connect, flags): + """Set device flags.""" + flags = flags.lower() + if flags.startswith("0b"): + flags = int(flags, 2) + elif flags.startswith("0x"): + flags = int(flags, 16) + else: + flags = int(flags) + return device.apply_flags(connect(), flags=flags) + + +@cli.command() +@click.option("-f", "--filename", default=None) +@click.pass_obj +def homescreen(connect, filename): + """Set new homescreen.""" + if filename is None: + img = b"\x00" + elif filename.endswith(".toif"): + img = open(filename, "rb").read() + if img[:8] != b"TOIf\x90\x00\x90\x00": + raise click.ClickException("File is not a TOIF file with size of 144x144") + else: + from PIL import Image + + im = Image.open(filename) + if im.size != (128, 64): + raise click.ClickException("Wrong size of the image") + im = im.convert("1") + pix = im.load() + img = bytearray(1024) + for j in range(64): + for i in range(128): + if pix[i, j]: + o = i + j * 128 + img[o // 8] |= 1 << (7 - o % 8) + img = bytes(img) + return device.apply_settings(connect(), homescreen=img) + + +# +# passphrase operations +# + + +@cli.group() +def passphrase(): + """Enable, disable or configure passphrase protection.""" + + +@passphrase.command(name="enabled") +@click.pass_obj +def passphrase_enable(connect): + """Enable passphrase.""" + return device.apply_settings(connect(), use_passphrase=True) + + +@passphrase.command(name="disabled") +@click.pass_obj +def passphrase_disable(connect): + """Disable passphrase.""" + return device.apply_settings(connect(), use_passphrase=False) + + +@passphrase.command(name="source") +@click.argument("source", type=ChoiceType(PASSPHRASE_SOURCE)) +@click.pass_obj +def passphrase_source(connect, source): + """Set passphrase source. + + Configure how to enter passphrase on Trezor Model T. The options are: + + \b + ask - always ask where to enter passphrase + device - always enter passphrase on device + host - always enter passphrase on host + """ + return device.apply_settings(connect(), passphrase_source=source) diff --git a/python/src/trezorlib/cli/stellar.py b/python/src/trezorlib/cli/stellar.py new file mode 100644 index 000000000..2fe2f9c80 --- /dev/null +++ b/python/src/trezorlib/cli/stellar.py @@ -0,0 +1,76 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import base64 + +import click + +from .. import stellar, tools + +PATH_HELP = "BIP32 path. Always use hardened paths and the m/44'/148'/ prefix" + + +@click.group(name="stellar") +def cli(): + """Stellar commands.""" + + +@cli.command() +@click.option( + "-n", + "--address", + required=False, + help=PATH_HELP, + default=stellar.DEFAULT_BIP32_PATH, +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Stellar public address""" + client = connect() + address_n = tools.parse_path(address) + return stellar.get_address(client, address_n, show_display) + + +@cli.command() +@click.option( + "-n", + "--address", + required=False, + help=PATH_HELP, + default=stellar.DEFAULT_BIP32_PATH, +) +@click.option( + "-n", + "--network-passphrase", + default=stellar.DEFAULT_NETWORK_PASSPHRASE, + required=False, + help="Network passphrase (blank for public network).", +) +@click.argument("b64envelope") +@click.pass_obj +def sign_transaction(connect, b64envelope, address, network_passphrase): + """Sign a base64-encoded transaction envelope + + For testnet transactions, use the following network passphrase: + 'Test SDF Network ; September 2015' + """ + client = connect() + address_n = tools.parse_path(address) + tx, operations = stellar.parse_transaction_bytes(base64.b64decode(b64envelope)) + resp = stellar.sign_tx(client, tx, operations, address_n, network_passphrase) + + return base64.b64encode(resp.signature) diff --git a/python/src/trezorlib/cli/tezos.py b/python/src/trezorlib/cli/tezos.py new file mode 100644 index 000000000..12660e977 --- /dev/null +++ b/python/src/trezorlib/cli/tezos.py @@ -0,0 +1,68 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import json + +import click + +from .. import messages, protobuf, tezos, tools + +PATH_HELP = "BIP-32 path, e.g. m/44'/1729'/0'" + + +@click.group(name="tezos") +def cli(): + """Tezos commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_address(connect, address, show_display): + """Get Tezos address for specified path.""" + client = connect() + address_n = tools.parse_path(address) + return tezos.get_address(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def get_public_key(connect, address, show_display): + """Get Tezos public key.""" + client = connect() + address_n = tools.parse_path(address) + return tezos.get_public_key(client, address_n, show_display) + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option( + "-f", + "--file", + type=click.File("r"), + default="-", + help="Transaction in JSON format (byte fields should be hexlified)", +) +@click.pass_obj +def sign_tx(connect, address, file): + """Sign Tezos transaction.""" + client = connect() + address_n = tools.parse_path(address) + msg = protobuf.dict_to_proto(messages.TezosSignTx, json.load(file)) + return tezos.sign_tx(client, address_n, msg) diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py index 2925976d6..d97383d04 100755 --- a/python/src/trezorlib/cli/trezorctl.py +++ b/python/src/trezorlib/cli/trezorctl.py @@ -2,158 +2,133 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2017 Marek Palatinus -# Copyright (C) 2012-2017 Pavol Rusnak -# Copyright (C) 2016-2017 Jochen Hoenicke -# Copyright (C) 2017 mruddy +# Copyright (C) 2012-2019 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 as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# 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 GNU Lesser General Public License -# along with this library. If not, see . +# You should have received a copy of the License along with this library. +# If not, see . -import base64 import json import os -import re import sys -from decimal import Decimal import click -import requests -from trezorlib import ( +from .. import coins, log, messages, protobuf, ui +from ..client import TrezorClient +from ..transport import enumerate_devices, get_transport +from . import ( binance, btc, cardano, - coins, cosi, - debuglink, + crypto, device, eos, ethereum, - exceptions, firmware, lisk, - log, - messages as proto, - misc, monero, nem, - protobuf, ripple, + settings, stellar, tezos, - tools, - ui, webauthn, ) -from trezorlib.client import TrezorClient -from trezorlib.transport import enumerate_devices, get_transport -try: - import rlp - import web3 - - ETHEREUM_SIGN_TX = True -except Exception: - ETHEREUM_SIGN_TX = False - - -class ChoiceType(click.Choice): - def __init__(self, typemap): - super(ChoiceType, self).__init__(typemap.keys()) - self.typemap = typemap - - def convert(self, value, param, ctx): - value = super(ChoiceType, self).convert(value, param, ctx) - return self.typemap[value] - - -CHOICE_PASSPHRASE_SOURCE_TYPE = ChoiceType( - { - "ask": proto.PassphraseSourceType.ASK, - "device": proto.PassphraseSourceType.DEVICE, - "host": proto.PassphraseSourceType.HOST, - } -) - - -CHOICE_DISPLAY_ROTATION_TYPE = ChoiceType( - {"north": 0, "east": 90, "south": 180, "west": 270} -) - - -CHOICE_RECOVERY_DEVICE_TYPE = ChoiceType( - { - "scrambled": proto.RecoveryDeviceType.ScrambledWords, - "matrix": proto.RecoveryDeviceType.Matrix, - } -) - -CHOICE_INPUT_SCRIPT_TYPE = ChoiceType( - { - "address": proto.InputScriptType.SPENDADDRESS, - "segwit": proto.InputScriptType.SPENDWITNESS, - "p2shsegwit": proto.InputScriptType.SPENDP2SHWITNESS, - } -) - -CHOICE_OUTPUT_SCRIPT_TYPE = ChoiceType( - { - "address": proto.OutputScriptType.PAYTOADDRESS, - "segwit": proto.OutputScriptType.PAYTOWITNESS, - "p2shsegwit": proto.OutputScriptType.PAYTOP2SHWITNESS, - } -) - -CHOICE_RESET_DEVICE_TYPE = ChoiceType( - { - "single": proto.BackupType.Bip39, - "shamir": proto.BackupType.Slip39_Basic, - "advanced": proto.BackupType.Slip39_Advanced, - } -) +COMMAND_ALIASES = { + "change-pin": settings.pin, + "enable-passphrase": settings.passphrase_enable, + "disable-passphrase": settings.passphrase_disable, + "set-passphrase-source": settings.passphrase_source, + "wipe-device": device.wipe, + "reset-device": device.setup, + "recovery-device": device.recover, + "backup-device": device.backup, + "sd-protect": device.sd_protect, + "load-device": device.load, + "self-test": device.self_test, + "get-entropy": crypto.get_entropy, + "encrypt-keyvalue": crypto.encrypt_keyvalue, + "decrypt-keyvalue": crypto.decrypt_keyvalue, + # currency name aliases: + "bnb": binance.cli, + "eth": ethereum.cli, + "ada": cardano.cli, + "lsk": lisk.cli, + "xmr": monero.cli, + "xrp": ripple.cli, + "xlm": stellar.cli, + "xtz": tezos.cli, +} -CHOICE_SD_PROTECT_OPERATION_TYPE = ChoiceType( - { - "enable": proto.SdProtectOperationType.ENABLE, - "disable": proto.SdProtectOperationType.DISABLE, - "refresh": proto.SdProtectOperationType.REFRESH, - } -) +class TrezorctlGroup(click.Group): + """Command group that handles compatibility for trezorctl. -class UnderscoreAgnosticGroup(click.Group): - """Command group that normalizes dashes and underscores. + The purpose is twofold: convert underscores to dashes, and ensure old-style commands + still work with new-style groups. Click 7.0 silently switched all underscore_commands to dash-commands. This implementation of `click.Group` responds to underscore_commands by invoking the respective dash-command. + + With trezorctl 0.11.5, we started to convert old-style long commands + (such as "binance-sign-tx") to command groups ("binance") with subcommands + ("sign-tx"). The `TrezorctlGroup` can perform subcommand lookup: if a command + "binance-sign-tx" does not exist in the default group, it tries to find "sign-tx" + subcommand of "binance" group. """ def get_command(self, ctx, cmd_name): + cmd_name = cmd_name.replace("_", "-") + # try to look up the real name cmd = super().get_command(ctx, cmd_name) - if cmd is None: - cmd = super().get_command(ctx, cmd_name.replace("_", "-")) - return cmd + if cmd: + return cmd + + # look for a backwards compatibility alias + if cmd_name in COMMAND_ALIASES: + return COMMAND_ALIASES[cmd_name] + + # look for subcommand in btc - "sign-tx" is now "btc sign-tx" + cmd = btc.cli.get_command(ctx, cmd_name) + if cmd: + return cmd + + # Old-style top-level commands looked like this: binance-sign-tx. + # We are moving to 'binance' command with 'sign-tx' subcommand. + try: + command, subcommand = cmd_name.split("-", maxsplit=1) + return super().get_command(ctx, command).get_command(ctx, subcommand) + except Exception: + pass + + # try to find a bitcoin-like coin whose shortcut matches the command + for coin in coins.coins_list: + if cmd_name.lower() == coin["shortcut"].lower(): + btc.DEFAULT_COIN = coin["coin_name"] + return btc.cli + + return None def configure_logging(verbose: int): if verbose: log.enable_debug_output(verbose) - log.OMITTED_MESSAGES.add(proto.Features) + log.OMITTED_MESSAGES.add(messages.Features) -@click.command(cls=UnderscoreAgnosticGroup, context_settings={"max_content_width": 400}) +@click.command(cls=TrezorctlGroup, context_settings={"max_content_width": 400}) @click.option( "-p", "--path", @@ -164,6 +139,7 @@ def configure_logging(verbose: int): @click.option( "-j", "--json", "is_json", is_flag=True, help="Print result as JSON object" ) +@click.version_option() @click.pass_context def cli(ctx, path, verbose, is_json): configure_logging(verbose) @@ -213,13 +189,15 @@ def print_result(res, path, verbose, is_json): # -@cli.command(name="list", help="List connected Trezor devices.") -def ls(): +@cli.command(name="list") +def list_devices(): + """List connected Trezor devices.""" return enumerate_devices() -@cli.command(help="Show version of trezorctl/trezorlib.") +@cli.command() def version(): + """Show version of trezorctl/trezorlib.""" from trezorlib import __version__ as VERSION return VERSION @@ -230,13 +208,14 @@ def version(): # -@cli.command(help="Send ping message.") +@cli.command() @click.argument("message") @click.option("-b", "--button-protection", is_flag=True) @click.option("-p", "--pin-protection", is_flag=True) @click.option("-r", "--passphrase-protection", is_flag=True) @click.pass_obj def ping(connect, message, button_protection, pin_protection, passphrase_protection): + """Send ping message.""" return connect().ping( message, button_protection=button_protection, @@ -245,620 +224,23 @@ def ping(connect, message, button_protection, pin_protection, passphrase_protect ) -@cli.command(help="Clear session (remove cached PIN, passphrase, etc.).") +@cli.command() @click.pass_obj def clear_session(connect): + """Clear session (remove cached PIN, passphrase, etc.).""" return connect().clear_session() -@cli.command(help="Get example entropy.") -@click.argument("size", type=int) -@click.pass_obj -def get_entropy(connect, size): - return misc.get_entropy(connect(), size).hex() - - -@cli.command(help="Retrieve device features and settings.") +@cli.command() @click.pass_obj def get_features(connect): + """Retrieve device features and settings.""" return connect().features -# -# Device management functions -# - - -@cli.command(help="Set, change or remove PIN.") -@click.option("-r", "--remove", is_flag=True) -@click.pass_obj -def change_pin(connect, remove): - return device.change_pin(connect(), remove) - - -@cli.command() -@click.argument("operation", type=CHOICE_SD_PROTECT_OPERATION_TYPE) -@click.pass_obj -def sd_protect(connect, operation): - """Secure the device with SD card protection. - - When SD card protection is enabled, a randomly generated secret is stored - on the SD card. During every PIN checking and unlocking operation this - secret is combined with the entered PIN value to decrypt data stored on - the device. The SD card will thus be needed every time you unlock the - device. The options are: - - \b - enable - Generate SD card secret and use it to protect the PIN and storage. - disable - Remove SD card secret protection. - refresh - Replace the current SD card secret with a new one. - """ - if connect().features.model == "1": - raise click.BadUsage("Trezor One does not support SD card protection.") - return device.sd_protect(connect(), operation) - - -@cli.command(help="Enable passphrase.") -@click.pass_obj -def enable_passphrase(connect): - return device.apply_settings(connect(), use_passphrase=True) - - -@cli.command(help="Disable passphrase.") -@click.pass_obj -def disable_passphrase(connect): - return device.apply_settings(connect(), use_passphrase=False) - - -@cli.command(help="Set new device label.") -@click.option("-l", "--label") -@click.pass_obj -def set_label(connect, label): - return device.apply_settings(connect(), label=label) - - -@cli.command() -@click.argument("source", type=CHOICE_PASSPHRASE_SOURCE_TYPE) -@click.pass_obj -def set_passphrase_source(connect, source): - """Set passphrase source. - - Configure how to enter passphrase on Trezor Model T. The options are: - - \b - ask - always ask where to enter passphrase - device - always enter passphrase on device - host - always enter passphrase on host - """ - return device.apply_settings(connect(), passphrase_source=source) - - -@cli.command() -@click.argument("rotation", type=CHOICE_DISPLAY_ROTATION_TYPE) -@click.pass_obj -def set_display_rotation(connect, rotation): - """Set display rotation. - - Configure display rotation for Trezor Model T. The options are - north, east, south or west. - """ - return device.apply_settings(connect(), display_rotation=rotation) - - -@cli.command(help="Set auto-lock delay (in seconds).") -@click.argument("delay", type=str) -@click.pass_obj -def set_auto_lock_delay(connect, delay): - value, unit = delay[:-1], delay[-1:] - units = {"s": 1, "m": 60, "h": 3600} - if unit in units: - seconds = float(value) * units[unit] - else: - seconds = float(delay) # assume seconds if no unit is specified - return device.apply_settings(connect(), auto_lock_delay_ms=int(seconds * 1000)) - - -@cli.command(help="Set device flags.") -@click.argument("flags") -@click.pass_obj -def set_flags(connect, flags): - flags = flags.lower() - if flags.startswith("0b"): - flags = int(flags, 2) - elif flags.startswith("0x"): - flags = int(flags, 16) - else: - flags = int(flags) - return device.apply_flags(connect(), flags=flags) - - -@cli.command(help="Set new homescreen.") -@click.option("-f", "--filename", default=None) -@click.pass_obj -def set_homescreen(connect, filename): - if filename is None: - img = b"\x00" - elif filename.endswith(".toif"): - img = open(filename, "rb").read() - if img[:8] != b"TOIf\x90\x00\x90\x00": - raise tools.CallException( - proto.FailureType.DataError, - "File is not a TOIF file with size of 144x144", - ) - else: - from PIL import Image - - im = Image.open(filename) - if im.size != (128, 64): - raise tools.CallException( - proto.FailureType.DataError, "Wrong size of the image" - ) - im = im.convert("1") - pix = im.load() - img = bytearray(1024) - for j in range(64): - for i in range(128): - if pix[i, j]: - o = i + j * 128 - img[o // 8] |= 1 << (7 - o % 8) - img = bytes(img) - return device.apply_settings(connect(), homescreen=img) - - -@cli.command(help="Set U2F counter.") -@click.argument("counter", type=int) -@click.pass_obj -def set_u2f_counter(connect, counter): - return device.set_u2f_counter(connect(), counter) - - -@cli.command(help="Get U2F counter.") -@click.pass_obj -def get_next_u2f_counter(connect): - return device.get_next_u2f_counter(connect()) - - -@cli.command(help="Reset device to factory defaults and remove all private data.") -@click.option( - "-b", - "--bootloader", - help="Wipe device in bootloader mode. This also erases the firmware.", - is_flag=True, -) -@click.pass_obj -def wipe_device(connect, bootloader): - client = connect() - if bootloader: - if not client.features.bootloader_mode: - click.echo("Please switch your device to bootloader mode.") - sys.exit(1) - else: - click.echo("Wiping user data and firmware!") - else: - if client.features.bootloader_mode: - click.echo( - "Your device is in bootloader mode. This operation would also erase firmware." - ) - click.echo( - 'Specify "--bootloader" if that is what you want, or disconnect and reconnect device in normal mode.' - ) - click.echo("Aborting.") - sys.exit(1) - else: - click.echo("Wiping user data!") - - try: - return device.wipe(connect()) - except tools.CallException as e: - click.echo("Action failed: {} {}".format(*e.args)) - sys.exit(3) - - -@cli.command(help="Load custom configuration to the device.") -@click.option("-m", "--mnemonic", multiple=True) -@click.option("-p", "--pin", default="") -@click.option("-r", "--passphrase-protection", is_flag=True) -@click.option("-l", "--label", default="") -@click.option("-i", "--ignore-checksum", is_flag=True) -@click.option("-s", "--slip0014", is_flag=True) -@click.pass_obj -def load_device( - connect, - mnemonic, - expand, - xprv, - pin, - passphrase_protection, - label, - ignore_checksum, - slip0014, -): - if slip0014 and mnemonic: - raise click.ClickException("Cannot use -s and -m together.") - - client = connect() - - if slip0014: - mnemonic = [" ".join(["all"] * 12)] - if not label: - label = "SLIP-0014" - - return debuglink.load_device( - client, - list(mnemonic), - pin, - passphrase_protection, - label, - "english", - ignore_checksum, - ) - - -@cli.command(help="Start safe recovery workflow.") -@click.option("-w", "--words", type=click.Choice(["12", "18", "24"]), default="24") -@click.option("-e", "--expand", is_flag=True) -@click.option("-p", "--pin-protection", is_flag=True) -@click.option("-r", "--passphrase-protection", is_flag=True) -@click.option("-l", "--label") -@click.option( - "-t", "--type", "rec_type", type=CHOICE_RECOVERY_DEVICE_TYPE, default="scrambled" -) -@click.option("-d", "--dry-run", is_flag=True) -@click.pass_obj -def recovery_device( - connect, - words, - expand, - pin_protection, - passphrase_protection, - label, - rec_type, - dry_run, -): - if rec_type == proto.RecoveryDeviceType.ScrambledWords: - input_callback = ui.mnemonic_words(expand) - else: - input_callback = ui.matrix_words - click.echo(ui.RECOVERY_MATRIX_DESCRIPTION) - - return device.recover( - connect(), - word_count=int(words), - passphrase_protection=passphrase_protection, - pin_protection=pin_protection, - label=label, - language="english", - input_callback=input_callback, - type=rec_type, - dry_run=dry_run, - ) - - -@cli.command(help="Perform device setup and generate new seed.") -@click.option("-e", "--show-entropy", is_flag=True) -@click.option("-t", "--strength", type=click.Choice(["128", "192", "256"])) -@click.option("-r", "--passphrase-protection", is_flag=True) -@click.option("-p", "--pin-protection", is_flag=True) -@click.option("-l", "--label") -@click.option("-u", "--u2f-counter", default=0) -@click.option("-s", "--skip-backup", is_flag=True) -@click.option("-n", "--no-backup", is_flag=True) -@click.option("-b", "--backup-type", type=CHOICE_RESET_DEVICE_TYPE, default="single") -@click.pass_obj -def reset_device( - connect, - show_entropy, - strength, - passphrase_protection, - pin_protection, - label, - u2f_counter, - skip_backup, - no_backup, - backup_type, -): - if strength: - strength = int(strength) - - client = connect() - if ( - backup_type == proto.BackupType.Slip39_Basic - and proto.Capability.Shamir not in client.features.capabilities - ) or ( - backup_type == proto.BackupType.Slip39_Advanced - and proto.Capability.ShamirGroups not in client.features.capabilities - ): - click.echo( - "WARNING: Your Trezor device does not indicate support for the requested\n" - "backup type. Traditional single-seed backup may be generated instead." - ) - - return device.reset( - client, - display_random=show_entropy, - strength=strength, - passphrase_protection=passphrase_protection, - pin_protection=pin_protection, - label=label, - language="english", - u2f_counter=u2f_counter, - skip_backup=skip_backup, - no_backup=no_backup, - backup_type=backup_type, - ) - - -@cli.command(help="Perform device seed backup.") -@click.pass_obj -def backup_device(connect): - return device.backup(connect()) - - -# -# Firmware update -# - - -ALLOWED_FIRMWARE_FORMATS = { - 1: (firmware.FirmwareFormat.TREZOR_ONE, firmware.FirmwareFormat.TREZOR_ONE_V2), - 2: (firmware.FirmwareFormat.TREZOR_T,), -} - - -def _print_version(version): - vstr = "Firmware version {major}.{minor}.{patch} build {build}".format(**version) - click.echo(vstr) - - -def validate_firmware(version, fw, expected_fingerprint=None): - if version == firmware.FirmwareFormat.TREZOR_ONE: - if fw.embedded_onev2: - click.echo("Trezor One firmware with embedded v2 image (1.8.0 or later)") - _print_version(fw.embedded_onev2.firmware_header.version) - else: - click.echo("Trezor One firmware image.") - elif version == firmware.FirmwareFormat.TREZOR_ONE_V2: - click.echo("Trezor One v2 firmware (1.8.0 or later)") - _print_version(fw.firmware_header.version) - elif version == firmware.FirmwareFormat.TREZOR_T: - click.echo("Trezor T firmware image.") - vendor = fw.vendor_header.vendor_string - vendor_version = "{major}.{minor}".format(**fw.vendor_header.version) - click.echo("Vendor header from {}, version {}".format(vendor, vendor_version)) - _print_version(fw.firmware_header.version) - - try: - firmware.validate(version, fw, allow_unsigned=False) - click.echo("Signatures are valid.") - except firmware.Unsigned: - if not click.confirm("No signatures found. Continue?", default=False): - sys.exit(1) - try: - firmware.validate(version, fw, allow_unsigned=True) - click.echo("Unsigned firmware looking OK.") - except firmware.FirmwareIntegrityError as e: - click.echo(e) - click.echo("Firmware validation failed, aborting.") - sys.exit(4) - except firmware.FirmwareIntegrityError as e: - click.echo(e) - click.echo("Firmware validation failed, aborting.") - sys.exit(4) - - fingerprint = firmware.digest(version, fw).hex() - click.echo("Firmware fingerprint: {}".format(fingerprint)) - if expected_fingerprint and fingerprint != expected_fingerprint: - click.echo("Expected fingerprint: {}".format(expected_fingerprint)) - click.echo("Fingerprints do not match, aborting.") - sys.exit(5) - - -def find_best_firmware_version( - bootloader_version, requested_version=None, beta=False, bitcoin_only=False -): - if beta: - url = "https://beta-wallet.trezor.io/data/firmware/{}/releases.json" - else: - url = "https://wallet.trezor.io/data/firmware/{}/releases.json" - releases = requests.get(url.format(bootloader_version[0])).json() - if not releases: - raise click.ClickException("Failed to get list of releases") - - if bitcoin_only: - releases = [r for r in releases if "url_bitcoinonly" in r] - releases.sort(key=lambda r: r["version"], reverse=True) - - def version_str(version): - return ".".join(map(str, version)) - - want_version = requested_version - - if want_version is None: - want_version = releases[0]["version"] - click.echo("Best available version: {}".format(version_str(want_version))) - - confirm_different_version = False - while True: - want_version_str = version_str(want_version) - try: - release = next(r for r in releases if r["version"] == want_version) - except StopIteration: - click.echo("Version {} not found.".format(want_version_str)) - sys.exit(1) - - if ( - "min_bootloader_version" in release - and release["min_bootloader_version"] > bootloader_version - ): - need_version_str = version_str(release["min_firmware_version"]) - click.echo( - "Version {} is required before upgrading to {}.".format( - need_version_str, want_version_str - ) - ) - want_version = release["min_firmware_version"] - confirm_different_version = True - else: - break - - if confirm_different_version: - installing_different = "Installing version {} instead.".format(want_version_str) - if requested_version is None: - click.echo(installing_different) - else: - ok = click.confirm(installing_different + " Continue?", default=True) - if not ok: - sys.exit(1) - - if bitcoin_only: - url = release["url_bitcoinonly"] - fingerprint = release["fingerprint_bitcoinonly"] - else: - url = release["url"] - fingerprint = release["fingerprint"] - if beta: - url = "https://beta-wallet.trezor.io/" + url - else: - url = "https://wallet.trezor.io/" + url - - return url, fingerprint - - -@cli.command() -# fmt: off -@click.option("-f", "--filename") -@click.option("-u", "--url") -@click.option("-v", "--version") -@click.option("-s", "--skip-check", is_flag=True, help="Do not validate firmware integrity") -@click.option("-n", "--dry-run", is_flag=True, help="Perform all steps but do not actually upload the firmware") -@click.option("--beta", is_flag=True, help="Use firmware from BETA wallet") -@click.option("--bitcoin-only", is_flag=True, help="Use bitcoin-only firmware (if possible)") -@click.option("--raw", is_flag=True, help="Push raw data to Trezor") -@click.option("--fingerprint", help="Expected firmware fingerprint in hex") -@click.option("--skip-vendor-header", help="Skip vendor header validation on Trezor T") -# fmt: on -@click.pass_obj -def firmware_update( - connect, - filename, - url, - version, - skip_check, - fingerprint, - skip_vendor_header, - raw, - dry_run, - beta, - bitcoin_only, -): - """Upload new firmware to device. - - Device must be in bootloader mode. - - You can specify a filename or URL from which the firmware can be downloaded. - You can also explicitly specify a firmware version that you want. - Otherwise, trezorctl will attempt to find latest available version - from wallet.trezor.io. - - If you provide a fingerprint via the --fingerprint option, it will be checked - against downloaded firmware fingerprint. Otherwise fingerprint is checked - against wallet.trezor.io information, if available. - - If you are customizing Model T bootloader and providing your own vendor header, - you can use --skip-vendor-header to ignore vendor header signatures. - """ - if sum(bool(x) for x in (filename, url, version)) > 1: - click.echo("You can use only one of: filename, url, version.") - sys.exit(1) - - client = connect() - if not dry_run and not client.features.bootloader_mode: - click.echo("Please switch your device to bootloader mode.") - sys.exit(1) - - f = client.features - bootloader_onev2 = f.major_version == 1 and f.minor_version >= 8 - - if filename: - data = open(filename, "rb").read() - else: - if not url: - bootloader_version = [f.major_version, f.minor_version, f.patch_version] - version_list = [int(x) for x in version.split(".")] if version else None - url, fp = find_best_firmware_version( - bootloader_version, version_list, beta, bitcoin_only - ) - if not fingerprint: - fingerprint = fp - - try: - click.echo("Downloading from {}".format(url)) - r = requests.get(url) - r.raise_for_status() - except requests.exceptions.HTTPError as err: - click.echo("Error downloading file: {}".format(err)) - sys.exit(3) - - data = r.content - - if not raw and not skip_check: - try: - version, fw = firmware.parse(data) - except Exception as e: - click.echo(e) - sys.exit(2) - - validate_firmware(version, fw, fingerprint) - if ( - bootloader_onev2 - and version == firmware.FirmwareFormat.TREZOR_ONE - and not fw.embedded_onev2 - ): - click.echo("Firmware is too old for your device. Aborting.") - sys.exit(3) - elif not bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE_V2: - click.echo("You need to upgrade to bootloader 1.8.0 first.") - sys.exit(3) - - if f.major_version not in ALLOWED_FIRMWARE_FORMATS: - click.echo("trezorctl doesn't know your device version. Aborting.") - sys.exit(3) - elif version not in ALLOWED_FIRMWARE_FORMATS[f.major_version]: - click.echo("Firmware does not match your device, aborting.") - sys.exit(3) - - if not raw: - # special handling for embedded-OneV2 format: - # for bootloader < 1.8, keep the embedding - # for bootloader 1.8.0 and up, strip the old OneV1 header - if bootloader_onev2 and data[:4] == b"TRZR" and data[256 : 256 + 4] == b"TRZF": - click.echo("Extracting embedded firmware image (fingerprint may change).") - data = data[256:] - - if dry_run: - click.echo("Dry run. Not uploading firmware to device.") - else: - try: - if f.major_version == 1 and f.firmware_present is not False: - # Trezor One does not send ButtonRequest - click.echo("Please confirm the action on your Trezor device") - return firmware.update(client, data) - except exceptions.Cancelled: - click.echo("Update aborted on device.") - except exceptions.TrezorException as e: - click.echo("Update failed: {}".format(e)) - sys.exit(3) - - -@cli.command(help="Perform a self-test.") -@click.pass_obj -def self_test(connect): - return debuglink.self_test(connect()) - - @cli.command() def usb_reset(): - """Perform USB reset on a stuck device. + """Perform USB reset on stuck devices. This can fix LIBUSB_ERROR_PIPE and similar errors when connecting to a device in a messed state. @@ -872,1147 +254,24 @@ def usb_reset(): # Basic coin functions # - -@cli.command(help="Get address for specified path.") -@click.option("-c", "--coin", default="Bitcoin") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" -) -@click.option("-t", "--script-type", type=CHOICE_INPUT_SCRIPT_TYPE, default="address") -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def get_address(connect, coin, address, script_type, show_display): - client = connect() - address_n = tools.parse_path(address) - return btc.get_address( - client, coin, address_n, show_display, script_type=script_type - ) - - -@cli.command(help="Get public node of given path.") -@click.option("-c", "--coin", default="Bitcoin") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'") -@click.option("-e", "--curve") -@click.option("-t", "--script-type", type=CHOICE_INPUT_SCRIPT_TYPE, default="address") -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def get_public_node(connect, coin, address, curve, script_type, show_display): - client = connect() - address_n = tools.parse_path(address) - result = btc.get_public_node( - client, - address_n, - ecdsa_curve_name=curve, - show_display=show_display, - coin_name=coin, - script_type=script_type, - ) - return { - "node": { - "depth": result.node.depth, - "fingerprint": "%08x" % result.node.fingerprint, - "child_num": result.node.child_num, - "chain_code": result.node.chain_code.hex(), - "public_key": result.node.public_key.hex(), - }, - "xpub": result.xpub, - } - - -# -# Signing options -# - - -@cli.command(help="Sign transaction.") -@click.option("-c", "--coin", default="Bitcoin") -@click.argument("json_file", type=click.File(), required=False) -@click.pass_obj -def sign_tx(connect, coin, json_file): - client = connect() - - # XXX this is the future code of this function - if json_file is not None: - data = json.load(json_file) - coin = data["coin_name"] - details = protobuf.dict_to_proto(proto.SignTx, data["details"]) - inputs = [protobuf.dict_to_proto(proto.TxInputType, i) for i in data["inputs"]] - outputs = [ - protobuf.dict_to_proto(proto.TxOutputType, output) - for output in data["outputs"] - ] - prev_txes = { - bytes.fromhex(txid): protobuf.dict_to_proto(proto.TransactionType, tx) - for txid, tx in data["prev_txes"].items() - } - - _, serialized_tx = btc.sign_tx( - client, coin, inputs, outputs, details, prev_txes - ) - - client.close() - - click.echo() - click.echo("Signed Transaction:") - click.echo(serialized_tx.hex()) - return - - # XXX ALL THE REST is legacy code and will be dropped - click.echo("Warning: interactive sign-tx mode is deprecated.", err=True) - click.echo( - "Instead, you should format your transaction data as JSON and " - "supply the file as an argument to sign-tx" - ) - if coin in coins.tx_api: - coin_data = coins.by_name[coin] - txapi = coins.tx_api[coin] - else: - click.echo('Coin "%s" is not recognized.' % coin, err=True) - click.echo( - "Supported coin types: %s" % ", ".join(coins.tx_api.keys()), err=True - ) - sys.exit(1) - - def default_script_type(address_n): - script_type = "address" - - if address_n is None: - pass - elif address_n[0] == tools.H_(49): - script_type = "p2shsegwit" - - return script_type - - def outpoint(s): - txid, vout = s.split(":") - return bytes.fromhex(txid), int(vout) - - inputs = [] - txes = {} - while True: - click.echo() - prev = click.prompt( - "Previous output to spend (txid:vout)", type=outpoint, default="" - ) - if not prev: - break - prev_hash, prev_index = prev - address_n = click.prompt("BIP-32 path to derive the key", type=tools.parse_path) - try: - tx = txapi[prev_hash] - txes[prev_hash] = tx - amount = tx.bin_outputs[prev_index].amount - click.echo("Prefilling input amount: {}".format(amount)) - except Exception as e: - print(e) - click.echo("Failed to fetch transation. This might bite you later.") - amount = click.prompt("Input amount (satoshis)", type=int, default=0) - sequence = click.prompt( - "Sequence Number to use (RBF opt-in enabled by default)", - type=int, - default=0xFFFFFFFD, - ) - script_type = click.prompt( - "Input type", - type=CHOICE_INPUT_SCRIPT_TYPE, - default=default_script_type(address_n), - ) - script_type = ( - script_type - if isinstance(script_type, int) - else CHOICE_INPUT_SCRIPT_TYPE.typemap[script_type] - ) - - new_input = proto.TxInputType( - address_n=address_n, - prev_hash=prev_hash, - prev_index=prev_index, - amount=amount, - script_type=script_type, - sequence=sequence, - ) - if coin_data["bip115"]: - prev_output = txapi.get_tx(prev_hash.hex()).bin_outputs[prev_index] - new_input.prev_block_hash_bip115 = prev_output.block_hash - new_input.prev_block_height_bip115 = prev_output.block_height - - inputs.append(new_input) - - if coin_data["bip115"]: - current_block_height = txapi.current_height() - # Zencash recommendation for the better protection - block_height = current_block_height - 300 - block_hash = txapi.get_block_hash(block_height) - # Blockhash passed in reverse order - block_hash = block_hash[::-1] - else: - block_height = None - block_hash = None - - outputs = [] - while True: - click.echo() - address = click.prompt("Output address (for non-change output)", default="") - if address: - address_n = None - else: - address = None - address_n = click.prompt( - "BIP-32 path (for change output)", type=tools.parse_path, default="" - ) - if not address_n: - break - amount = click.prompt("Amount to spend (satoshis)", type=int) - script_type = click.prompt( - "Output type", - type=CHOICE_OUTPUT_SCRIPT_TYPE, - default=default_script_type(address_n), - ) - script_type = ( - script_type - if isinstance(script_type, int) - else CHOICE_OUTPUT_SCRIPT_TYPE.typemap[script_type] - ) - outputs.append( - proto.TxOutputType( - address_n=address_n, - address=address, - amount=amount, - script_type=script_type, - block_hash_bip115=block_hash, - block_height_bip115=block_height, - ) - ) - - signtx = proto.SignTx() - signtx.version = click.prompt("Transaction version", type=int, default=2) - signtx.lock_time = click.prompt("Transaction locktime", type=int, default=0) - if coin == "Capricoin": - signtx.timestamp = click.prompt("Transaction timestamp", type=int) - - _, serialized_tx = btc.sign_tx( - client, coin, inputs, outputs, details=signtx, prev_txes=txes - ) - - client.close() - - click.echo() - click.echo("Signed Transaction:") - click.echo(serialized_tx.hex()) - click.echo() - click.echo("Use the following form to broadcast it to the network:") - click.echo(txapi.pushtx_url) - - -# -# Message functions -# - - -@cli.command(help="Sign message using address of given path.") -@click.option("-c", "--coin", default="Bitcoin") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" -) -@click.option( - "-t", - "--script-type", - type=click.Choice(["address", "segwit", "p2shsegwit"]), - default="address", -) -@click.argument("message") -@click.pass_obj -def sign_message(connect, coin, address, message, script_type): - client = connect() - address_n = tools.parse_path(address) - typemap = { - "address": proto.InputScriptType.SPENDADDRESS, - "segwit": proto.InputScriptType.SPENDWITNESS, - "p2shsegwit": proto.InputScriptType.SPENDP2SHWITNESS, - } - script_type = typemap[script_type] - res = btc.sign_message(client, coin, address_n, message, script_type) - return { - "message": message, - "address": res.address, - "signature": base64.b64encode(res.signature), - } - - -@cli.command(help="Verify message.") -@click.option("-c", "--coin", default="Bitcoin") -@click.argument("address") -@click.argument("signature") -@click.argument("message") -@click.pass_obj -def verify_message(connect, coin, address, signature, message): - signature = base64.b64decode(signature) - return btc.verify_message(connect(), coin, address, signature, message) - - -@cli.command(help="Sign message with Ethereum address.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0" -) -@click.argument("message") -@click.pass_obj -def ethereum_sign_message(connect, address, message): - client = connect() - address_n = tools.parse_path(address) - ret = ethereum.sign_message(client, address_n, message) - output = { - "message": message, - "address": ret.address, - "signature": "0x%s" % ret.signature.hex(), - } - return output - - -def ethereum_decode_hex(value): - if value.startswith("0x") or value.startswith("0X"): - return bytes.fromhex(value[2:]) - else: - return bytes.fromhex(value) - - -@cli.command(help="Verify message signed with Ethereum address.") -@click.argument("address") -@click.argument("signature") -@click.argument("message") -@click.pass_obj -def ethereum_verify_message(connect, address, signature, message): - signature = ethereum_decode_hex(signature) - return ethereum.verify_message(connect(), address, signature, message) - - -@cli.command(help="Encrypt value by given key and path.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/10016'/0") -@click.argument("key") -@click.argument("value") -@click.pass_obj -def encrypt_keyvalue(connect, address, key, value): - client = connect() - address_n = tools.parse_path(address) - res = misc.encrypt_keyvalue(client, address_n, key, value.encode()) - return res.hex() - - -@cli.command(help="Decrypt value by given key and path.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/10016'/0") -@click.argument("key") -@click.argument("value") -@click.pass_obj -def decrypt_keyvalue(connect, address, key, value): - client = connect() - address_n = tools.parse_path(address) - return misc.decrypt_keyvalue(client, address_n, key, bytes.fromhex(value)) - - -# @cli.command(help='Encrypt message.') -# @click.option('-c', '--coin', default='Bitcoin') -# @click.option('-d', '--display-only', is_flag=True) -# @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") -# @click.argument('pubkey') -# @click.argument('message') -# @click.pass_obj -# def encrypt_message(connect, coin, display_only, address, pubkey, message): -# client = connect() -# pubkey = bytes.fromhex(pubkey) -# address_n = tools.parse_path(address) -# res = client.encrypt_message(pubkey, message, display_only, coin, address_n) -# return { -# 'nonce': res.nonce.hex(), -# 'message': res.message.hex(), -# 'hmac': res.hmac.hex(), -# 'payload': base64.b64encode(res.nonce + res.message + res.hmac), -# } - - -# @cli.command(help='Decrypt message.') -# @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") -# @click.argument('payload') -# @click.pass_obj -# def decrypt_message(connect, address, payload): -# client = connect() -# address_n = tools.parse_path(address) -# payload = base64.b64decode(payload) -# nonce, message, msg_hmac = payload[:33], payload[33:-8], payload[-8:] -# return client.decrypt_message(address_n, nonce, message, msg_hmac) - - -# -# Ethereum functions -# - - -@cli.command(help="Get Ethereum address in hex encoding.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def ethereum_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - return ethereum.get_address(client, address_n, show_display) - - -@cli.command(help="Get Ethereum public node of given path.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def ethereum_get_public_node(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - result = ethereum.get_public_node(client, address_n, show_display=show_display) - return { - "node": { - "depth": result.node.depth, - "fingerprint": "%08x" % result.node.fingerprint, - "child_num": result.node.child_num, - "chain_code": result.node.chain_code.hex(), - "public_key": result.node.public_key.hex(), - }, - "xpub": result.xpub, - } - - -# fmt: off -ETHER_UNITS = { - 'wei': 1, - 'kwei': 1000, - 'babbage': 1000, - 'femtoether': 1000, - 'mwei': 1000000, - 'lovelace': 1000000, - 'picoether': 1000000, - 'gwei': 1000000000, - 'shannon': 1000000000, - 'nanoether': 1000000000, - 'nano': 1000000000, - 'szabo': 1000000000000, - 'microether': 1000000000000, - 'micro': 1000000000000, - 'finney': 1000000000000000, - 'milliether': 1000000000000000, - 'milli': 1000000000000000, - 'ether': 1000000000000000000, - 'eth': 1000000000000000000, -} -# fmt: on - - -def ethereum_amount_to_int(ctx, param, value): - if value is None: - return None - if value.isdigit(): - return int(value) - try: - number, unit = re.match(r"^(\d+(?:.\d+)?)([a-z]+)", value).groups() - scale = ETHER_UNITS[unit] - decoded_number = Decimal(number) - return int(decoded_number * scale) - - except Exception: - import traceback - - traceback.print_exc() - raise click.BadParameter("Amount not understood") - - -def ethereum_list_units(ctx, param, value): - if not value or ctx.resilient_parsing: - return - maxlen = max(len(k) for k in ETHER_UNITS.keys()) + 1 - for unit, scale in ETHER_UNITS.items(): - click.echo("{:{maxlen}}: {}".format(unit, scale, maxlen=maxlen)) - ctx.exit() - - -def ethereum_erc20_contract(w3, token_address, to_address, amount): - min_abi = [ - { - "name": "transfer", - "type": "function", - "constant": False, - "inputs": [ - {"name": "_to", "type": "address"}, - {"name": "_value", "type": "uint256"}, - ], - "outputs": [{"name": "", "type": "bool"}], - } - ] - contract = w3.eth.contract(address=token_address, abi=min_abi) - return contract.encodeABI("transfer", [to_address, amount]) - - -@cli.command() -@click.option( - "-c", "--chain-id", type=int, default=1, help="EIP-155 chain id (replay protection)" -) -@click.option( - "-n", - "--address", - required=True, - help="BIP-32 path to source address, e.g., m/44'/60'/0'/0/0", -) -@click.option( - "-g", "--gas-limit", type=int, help="Gas limit (required for offline signing)" -) -@click.option( - "-t", - "--gas-price", - help="Gas price (required for offline signing)", - callback=ethereum_amount_to_int, -) -@click.option( - "-i", "--nonce", type=int, help="Transaction counter (required for offline signing)" -) -@click.option("-d", "--data", help="Data as hex string, e.g. 0x12345678") -@click.option("-p", "--publish", is_flag=True, help="Publish transaction via RPC") -@click.option("-x", "--tx-type", type=int, help="TX type (used only for Wanchain)") -@click.option("-t", "--token", help="ERC20 token address") -@click.option( - "--list-units", - is_flag=True, - help="List known currency units and exit.", - is_eager=True, - callback=ethereum_list_units, - expose_value=False, -) -@click.argument("to_address") -@click.argument("amount", callback=ethereum_amount_to_int) -@click.pass_obj -def ethereum_sign_tx( - connect, - chain_id, - address, - amount, - gas_limit, - gas_price, - nonce, - data, - publish, - to_address, - tx_type, - token, -): - """Sign (and optionally publish) Ethereum transaction. - - Use TO_ADDRESS as destination address, or set to "" for contract creation. - - Specify a contract address with the --token option to send an ERC20 token. - - You can specify AMOUNT and gas price either as a number of wei, - or you can use a unit suffix. - - Use the --list-units option to show all known currency units. - ERC20 token amounts are specified in eth/wei, custom units are not supported. - - If any of gas price, gas limit and nonce is not specified, this command will - try to connect to an ethereum node and auto-fill these values. You can configure - the connection with WEB3_PROVIDER_URI environment variable. - """ - if not ETHEREUM_SIGN_TX: - click.echo("Ethereum requirements not installed.") - click.echo("Please run:") - click.echo() - click.echo(" pip install web3 rlp") - sys.exit(1) - - w3 = web3.Web3() - if ( - gas_price is None or gas_limit is None or nonce is None or publish - ) and not w3.isConnected(): - click.echo("Failed to connect to Ethereum node.") - click.echo( - "If you want to sign offline, make sure you provide --gas-price, " - "--gas-limit and --nonce arguments" - ) - sys.exit(1) - - if data is not None and token is not None: - click.echo("Can't send tokens and custom data at the same time") - sys.exit(1) - - client = connect() - address_n = tools.parse_path(address) - from_address = ethereum.get_address(client, address_n) - - if token: - data = ethereum_erc20_contract(w3, token, to_address, amount) - to_address = token - amount = 0 - - if data: - data = ethereum_decode_hex(data) - else: - data = b"" - - if gas_price is None: - gas_price = w3.eth.gasPrice - - if gas_limit is None: - gas_limit = w3.eth.estimateGas( - { - "to": to_address, - "from": from_address, - "value": amount, - "data": "0x%s" % data.hex(), - } - ) - - if nonce is None: - nonce = w3.eth.getTransactionCount(from_address) - - sig = ethereum.sign_tx( - client, - n=address_n, - tx_type=tx_type, - nonce=nonce, - gas_price=gas_price, - gas_limit=gas_limit, - to=to_address, - value=amount, - data=data, - chain_id=chain_id, - ) - - to = ethereum_decode_hex(to_address) - if tx_type is None: - transaction = rlp.encode((nonce, gas_price, gas_limit, to, amount, data) + sig) - else: - transaction = rlp.encode( - (tx_type, nonce, gas_price, gas_limit, to, amount, data) + sig - ) - tx_hex = "0x%s" % transaction.hex() - - if publish: - tx_hash = w3.eth.sendRawTransaction(tx_hex).hex() - return "Transaction published with ID: %s" % tx_hash - else: - return "Signed raw transaction:\n%s" % tx_hex - - -# -# EOS functions -# - - -@cli.command(help="Get Eos public key in base58 encoding.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/194'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def eos_get_public_key(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - res = eos.get_public_key(client, address_n, show_display) - return "WIF: {}\nRaw: {}".format(res.wif_public_key, res.raw_public_key.hex()) - - -@cli.command(help="Init sign (and optionally publish) EOS transaction. ") -@click.option( - "-n", - "--address", - required=True, - help="BIP-32 path to source address, e.g., m/44'/194'/0'/0/0", -) -@click.option( - "-f", - "--file", - type=click.File("r"), - required=True, - help="Transaction in JSON format", -) -@click.pass_obj -def eos_sign_transaction(connect, address, file): - client = connect() - - tx_json = json.load(file) - - address_n = tools.parse_path(address) - return eos.sign_tx(client, address_n, tx_json["transaction"], tx_json["chain_id"]) - - -# -# ADA functions -# - - -@cli.command(help="Sign Cardano transaction.") -@click.option( - "-f", - "--file", - type=click.File("r"), - required=True, - help="Transaction in JSON format", -) -@click.option("-N", "--network", type=int, default=1) -@click.pass_obj -def cardano_sign_tx(connect, file, network): - client = connect() - - transaction = json.load(file) - - inputs = [cardano.create_input(input) for input in transaction["inputs"]] - outputs = [cardano.create_output(output) for output in transaction["outputs"]] - transactions = transaction["transactions"] - - signed_transaction = cardano.sign_tx(client, inputs, outputs, transactions, network) - - return { - "tx_hash": signed_transaction.tx_hash.hex(), - "tx_body": signed_transaction.tx_body.hex(), - } - - -@cli.command(help="Get Cardano address.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def cardano_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - - return cardano.get_address(client, address_n, show_display) - - -@cli.command(help="Get Cardano public key.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" -) -@click.pass_obj -def cardano_get_public_key(connect, address): - client = connect() - address_n = tools.parse_path(address) - - return cardano.get_public_key(client, address_n) - - -# -# NEM functions -# - - -@cli.command(help="Get NEM address for specified path.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/43'/0'") -@click.option("-N", "--network", type=int, default=0x68) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def nem_get_address(connect, address, network, show_display): - client = connect() - address_n = tools.parse_path(address) - return nem.get_address(client, address_n, network, show_display) - - -@cli.command(help="Sign (and optionally broadcast) NEM transaction.") -@click.option("-n", "--address", help="BIP-32 path to signing key") -@click.option( - "-f", - "--file", - type=click.File("r"), - default="-", - help="Transaction in NIS (RequestPrepareAnnounce) format", -) -@click.option("-b", "--broadcast", help="NIS to announce transaction to") -@click.pass_obj -def nem_sign_tx(connect, address, file, broadcast): - client = connect() - address_n = tools.parse_path(address) - transaction = nem.sign_tx(client, address_n, json.load(file)) - - payload = {"data": transaction.data.hex(), "signature": transaction.signature.hex()} - - if broadcast: - return requests.post( - "{}/transaction/announce".format(broadcast), json=payload - ).json() - else: - return payload - - -# -# Lisk functions -# - - -@cli.command(help="Get Lisk address for specified path.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def lisk_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - return lisk.get_address(client, address_n, show_display) - - -@cli.command(help="Get Lisk public key for specified path.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def lisk_get_public_key(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - res = lisk.get_public_key(client, address_n, show_display) - output = {"public_key": res.public_key.hex()} - return output - - -@cli.command(help="Sign Lisk transaction.") -@click.option( - "-n", - "--address", - required=True, - help="BIP-32 path to signing key, e.g. m/44'/134'/0'/0'", -) -@click.option( - "-f", "--file", type=click.File("r"), default="-", help="Transaction in JSON format" -) -# @click.option('-b', '--broadcast', help='Broadcast Lisk transaction') -@click.pass_obj -def lisk_sign_tx(connect, address, file): - client = connect() - address_n = tools.parse_path(address) - transaction = lisk.sign_tx(client, address_n, json.load(file)) - - payload = {"signature": transaction.signature.hex()} - - return payload - - -@cli.command(help="Sign message with Lisk address.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'" -) -@click.argument("message") -@click.pass_obj -def lisk_sign_message(connect, address, message): - client = connect() - address_n = client.expand_path(address) - res = lisk.sign_message(client, address_n, message) - output = { - "message": message, - "public_key": res.public_key.hex(), - "signature": res.signature.hex(), - } - return output - - -@cli.command(help="Verify message signed with Lisk address.") -@click.argument("pubkey") -@click.argument("signature") -@click.argument("message") -@click.pass_obj -def lisk_verify_message(connect, pubkey, signature, message): - signature = bytes.fromhex(signature) - pubkey = bytes.fromhex(pubkey) - return lisk.verify_message(connect(), pubkey, signature, message) - - -# -# Monero functions -# - - -@cli.command(help="Get Monero address for specified path.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/128'/0'") -@click.option("-d", "--show-display", is_flag=True) -@click.option( - "-t", "--network-type", type=click.Choice(["0", "1", "2", "3"]), default="0" -) -@click.pass_obj -def monero_get_address(connect, address, show_display, network_type): - client = connect() - address_n = tools.parse_path(address) - network_type = int(network_type) - return monero.get_address(client, address_n, show_display, network_type) - - -@cli.command(help="Get Monero watch key for specified path.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/128'/0'") -@click.option( - "-t", "--network-type", type=click.Choice(["0", "1", "2", "3"]), default="0" -) -@click.pass_obj -def monero_get_watch_key(connect, address, network_type): - client = connect() - address_n = tools.parse_path(address) - network_type = int(network_type) - res = monero.get_watch_key(client, address_n, network_type) - output = {"address": res.address.decode(), "watch_key": res.watch_key.hex()} - return output - - -# -# CoSi functions -# - - -@cli.command(help="Ask device to commit to CoSi signing.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" -) -@click.argument("data") -@click.pass_obj -def cosi_commit(connect, address, data): - client = connect() - address_n = tools.parse_path(address) - return cosi.commit(client, address_n, bytes.fromhex(data)) - - -@cli.command(help="Ask device to sign using CoSi.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0" -) -@click.argument("data") -@click.argument("global_commitment") -@click.argument("global_pubkey") -@click.pass_obj -def cosi_sign(connect, address, data, global_commitment, global_pubkey): - client = connect() - address_n = tools.parse_path(address) - return cosi.sign( - client, - address_n, - bytes.fromhex(data), - bytes.fromhex(global_commitment), - bytes.fromhex(global_pubkey), - ) - - -# -# Stellar functions -# -@cli.command(help="Get Stellar public address") -@click.option( - "-n", - "--address", - required=False, - help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", - default=stellar.DEFAULT_BIP32_PATH, -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def stellar_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - return stellar.get_address(client, address_n, show_display) - - -@cli.command(help="Sign a base64-encoded transaction envelope") -@click.option( - "-n", - "--address", - required=False, - help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", - default=stellar.DEFAULT_BIP32_PATH, -) -@click.option( - "-n", - "--network-passphrase", - default=stellar.DEFAULT_NETWORK_PASSPHRASE, - required=False, - help="Network passphrase (blank for public network). Testnet is: 'Test SDF Network ; September 2015'", -) -@click.argument("b64envelope") -@click.pass_obj -def stellar_sign_transaction(connect, b64envelope, address, network_passphrase): - client = connect() - address_n = tools.parse_path(address) - tx, operations = stellar.parse_transaction_bytes(base64.b64decode(b64envelope)) - resp = stellar.sign_tx(client, tx, operations, address_n, network_passphrase) - - return base64.b64encode(resp.signature) - - -# -# Ripple functions -# -@cli.command(help="Get Ripple address") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/144'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def ripple_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - return ripple.get_address(client, address_n, show_display) - - -@cli.command(help="Sign Ripple transaction") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/144'/0'/0/0" -) -@click.option( - "-f", "--file", type=click.File("r"), default="-", help="Transaction in JSON format" -) -@click.pass_obj -def ripple_sign_tx(connect, address, file): - client = connect() - address_n = tools.parse_path(address) - msg = ripple.create_sign_tx_msg(json.load(file)) - - result = ripple.sign_tx(client, address_n, msg) - click.echo("Signature:") - click.echo(result.signature.hex()) - click.echo() - click.echo("Serialized tx including the signature:") - click.echo(result.serialized_tx.hex()) - - -# -# Tezos functions -# -@cli.command(help="Get Tezos address for specified path.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/1729'/0'") -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def tezos_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - return tezos.get_address(client, address_n, show_display) - - -@cli.command(help="Get Tezos public key.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/1729'/0'") -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def tezos_get_public_key(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - return tezos.get_public_key(client, address_n, show_display) - - -@cli.command(help="Sign Tezos transaction.") -@click.option("-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/1729'/0'") -@click.option( - "-f", - "--file", - type=click.File("r"), - default="-", - help="Transaction in JSON format (byte fields should be hexlified)", -) -@click.pass_obj -def tezos_sign_tx(connect, address, file): - client = connect() - address_n = tools.parse_path(address) - msg = protobuf.dict_to_proto(proto.TezosSignTx, json.load(file)) - return tezos.sign_tx(client, address_n, msg) - - -# -# Binance functions -# - - -@cli.command(help="Get Binance address for specified path.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/714'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def binance_get_address(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - - return binance.get_address(client, address_n, show_display) - - -@cli.command(help="Get Binance public key.") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/714'/0'/0/0" -) -@click.option("-d", "--show-display", is_flag=True) -@click.pass_obj -def binance_get_public_key(connect, address, show_display): - client = connect() - address_n = tools.parse_path(address) - - return binance.get_public_key(client, address_n, show_display).hex() - - -@cli.command(help="Sign Binance transaction") -@click.option( - "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/714'/0'/0/0" -) -@click.option( - "-f", - "--file", - type=click.File("r"), - required=True, - help="Transaction in JSON format", -) -@click.pass_obj -def binance_sign_tx(connect, address, file): - client = connect() - address_n = tools.parse_path(address) - - return binance.sign_tx(client, address_n, json.load(file)) - - -# -# WebAuthn functions -# - - -@cli.command(help="List all resident credentials on the device.") -@click.pass_obj -def webauthn_list_credentials(connect): - creds = webauthn.list_credentials(connect()) - for cred in creds: - click.echo("") - click.echo("WebAuthn credential at index {}:".format(cred.index)) - if cred.rp_id is not None: - click.echo(" Relying party ID: {}".format(cred.rp_id)) - if cred.rp_name is not None: - click.echo(" Relying party name: {}".format(cred.rp_name)) - if cred.user_id is not None: - click.echo(" User ID: {}".format(cred.user_id.hex())) - if cred.user_name is not None: - click.echo(" User name: {}".format(cred.user_name)) - if cred.user_display_name is not None: - click.echo(" User display name: {}".format(cred.user_display_name)) - if cred.creation_time is not None: - click.echo(" Creation time: {}".format(cred.creation_time)) - if cred.hmac_secret is not None: - click.echo(" hmac-secret enabled: {}".format(cred.hmac_secret)) - if cred.use_sign_count is not None: - click.echo(" Use signature counter: {}".format(cred.use_sign_count)) - click.echo(" Credential ID: {}".format(cred.id.hex())) - - if not creds: - click.echo("There are no resident credentials stored on the device.") - - -@cli.command() -@click.argument("hex_credential_id") -@click.pass_obj -def webauthn_add_credential(connect, hex_credential_id): - """Add the credential with the given ID as a resident credential. - - HEX_CREDENTIAL_ID is the credential ID as a hexadecimal string. - """ - return webauthn.add_credential(connect(), bytes.fromhex(hex_credential_id)) - - -@cli.command(help="Remove the resident credential at the given index.") -@click.option( - "-i", "--index", required=True, type=click.IntRange(0, 99), help="Credential index." -) -@click.pass_obj -def webauthn_remove_credential(connect, index): - return webauthn.remove_credential(connect(), index) +cli.add_command(binance.cli) +cli.add_command(btc.cli) +cli.add_command(cardano.cli) +cli.add_command(cosi.cli) +cli.add_command(crypto.cli) +cli.add_command(device.cli) +cli.add_command(eos.cli) +cli.add_command(ethereum.cli) +cli.add_command(lisk.cli) +cli.add_command(monero.cli) +cli.add_command(nem.cli) +cli.add_command(ripple.cli) +cli.add_command(settings.cli) +cli.add_command(stellar.cli) +cli.add_command(tezos.cli) +cli.add_command(webauthn.cli) + +cli.add_command(firmware.firmware_update) # diff --git a/python/src/trezorlib/cli/webauthn.py b/python/src/trezorlib/cli/webauthn.py new file mode 100644 index 000000000..e6bb3f493 --- /dev/null +++ b/python/src/trezorlib/cli/webauthn.py @@ -0,0 +1,110 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 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 . + +import click + +from .. import device, webauthn + + +@click.group(name="webauthn") +def cli(): + """WebAuthn, FIDO2 and U2F management commands.""" + + +@click.group() +def credentials(): + """Manage FIDO2 resident credentials.""" + + +@credentials.command(name="list") +@click.pass_obj +def credentials_list(connect): + """List all resident credentials on the device.""" + creds = webauthn.list_credentials(connect()) + for cred in creds: + click.echo("") + click.echo("WebAuthn credential at index {}:".format(cred.index)) + if cred.rp_id is not None: + click.echo(" Relying party ID: {}".format(cred.rp_id)) + if cred.rp_name is not None: + click.echo(" Relying party name: {}".format(cred.rp_name)) + if cred.user_id is not None: + click.echo(" User ID: {}".format(cred.user_id.hex())) + if cred.user_name is not None: + click.echo(" User name: {}".format(cred.user_name)) + if cred.user_display_name is not None: + click.echo(" User display name: {}".format(cred.user_display_name)) + if cred.creation_time is not None: + click.echo(" Creation time: {}".format(cred.creation_time)) + if cred.hmac_secret is not None: + click.echo(" hmac-secret enabled: {}".format(cred.hmac_secret)) + if cred.use_sign_count is not None: + click.echo(" Use signature counter: {}".format(cred.use_sign_count)) + click.echo(" Credential ID: {}".format(cred.id.hex())) + + if not creds: + click.echo("There are no resident credentials stored on the device.") + + +@credentials.command(name="add") +@click.argument("hex_credential_id") +@click.pass_obj +def credential_add(connect, hex_credential_id): + """Add the credential with the given ID as a resident credential. + + HEX_CREDENTIAL_ID is the credential ID as a hexadecimal string. + """ + return webauthn.add_credential(connect(), bytes.fromhex(hex_credential_id)) + + +@cli.command() +@click.option( + "-i", "--index", required=True, type=click.IntRange(0, 99), help="Credential index." +) +@click.pass_obj +def remove_credential(connect, index): + """Remove the resident credential at the given index.""" + return webauthn.remove_credential(connect(), index) + + +# +# U2F counter operations +# + + +@cli.group() +def u2f(): + """Get or set the U2F counter value.""" + + +@u2f.command(name="set") +@click.argument("counter", type=int) +@click.pass_obj +def u2f_set(connect, counter): + """Set U2F counter value.""" + return device.set_u2f_counter(connect(), counter) + + +@u2f.command(name="get-next") +@click.pass_obj +def u2f_get_next(connect): + """Get-and-increase value of U2F counter. + + U2F counter value cannot be read directly. On each U2F exchange, the counter value + is returned and atomically increased. This command performs the same operation + and returns the counter value. + """ + return device.get_next_u2f_counter(connect()) diff --git a/python/src/trezorlib/webauthn.py b/python/src/trezorlib/webauthn.py index 3ca88d1ae..97cd4fbbe 100644 --- a/python/src/trezorlib/webauthn.py +++ b/python/src/trezorlib/webauthn.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2019 SatoshiLabs and contributors +# Copyright (C) 2012-2019 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 @@ -14,7 +14,6 @@ # You should have received a copy of the License along with this library. # If not, see . - from . import messages as proto from .tools import expect