python/trezorctl: split trezorctl into separate modules

Instead of all commands (like `load-device`, `change-pin`,
`tezos-sign-tx`, `ethereum-verify-message`...) living in trezorctl.py,
each functional group is now defined in a separate file.

With that, better structuring of the trezorctl command becomes
available:
- instead of `trezorctl set-label`, use `trezorctl set label`
- instead of `trezorctl change-pin`, use `trezorctl set pin`
- instead of `trezorctl enable-passphrase`, use `trezorctl set
passphrase enabled`

For common commands, such as `sign-tx`, it is possible to use the
currency name or shortcut:

- `trezorctl btc sign-tx`
- `trezorctl ethereum sign-tx`
- `trezorctl xtz sign-tx`
- `trezorctl doge sign-tx`
etc.

Some aliases have been retained for better compatibility. For others,
refer to `trezorctl --help` and `trezorctl <command> --help`.
pull/805/head
matejcik 5 years ago committed by matejcik
parent d029920540
commit 8e4de5e929

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
from . import messages
from .protobuf import dict_to_proto
from .tools import expect, session

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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]

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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))

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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),
)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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))

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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"])

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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())

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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)

File diff suppressed because it is too large Load Diff

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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())

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
from . import messages as proto
from .tools import expect

Loading…
Cancel
Save