mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-11 16:00:57 +00:00
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`.
This commit is contained in:
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 . import messages
|
||||||
from .protobuf import dict_to_proto
|
from .protobuf import dict_to_proto
|
||||||
from .tools import expect, session
|
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]
|
70
python/src/trezorlib/cli/binance.py
Normal file
70
python/src/trezorlib/cli/binance.py
Normal file
@ -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))
|
324
python/src/trezorlib/cli/btc.py
Normal file
324
python/src/trezorlib/cli/btc.py
Normal 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)
|
79
python/src/trezorlib/cli/cardano.py
Normal file
79
python/src/trezorlib/cli/cardano.py
Normal file
@ -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)
|
56
python/src/trezorlib/cli/cosi.py
Normal file
56
python/src/trezorlib/cli/cosi.py
Normal file
@ -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),
|
||||||
|
)
|
57
python/src/trezorlib/cli/crypto.py
Normal file
57
python/src/trezorlib/cli/crypto.py
Normal file
@ -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))
|
262
python/src/trezorlib/cli/device.py
Normal file
262
python/src/trezorlib/cli/device.py
Normal file
@ -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)
|
60
python/src/trezorlib/cli/eos.py
Normal file
60
python/src/trezorlib/cli/eos.py
Normal file
@ -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"])
|
321
python/src/trezorlib/cli/ethereum.py
Normal file
321
python/src/trezorlib/cli/ethereum.py
Normal file
@ -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)
|
272
python/src/trezorlib/cli/firmware.py
Normal file
272
python/src/trezorlib/cli/firmware.py
Normal file
@ -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)
|
99
python/src/trezorlib/cli/lisk.py
Normal file
99
python/src/trezorlib/cli/lisk.py
Normal file
@ -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)
|
57
python/src/trezorlib/cli/monero.py
Normal file
57
python/src/trezorlib/cli/monero.py
Normal file
@ -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
|
68
python/src/trezorlib/cli/nem.py
Normal file
68
python/src/trezorlib/cli/nem.py
Normal file
@ -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
|
59
python/src/trezorlib/cli/ripple.py
Normal file
59
python/src/trezorlib/cli/ripple.py
Normal file
@ -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())
|
161
python/src/trezorlib/cli/settings.py
Normal file
161
python/src/trezorlib/cli/settings.py
Normal file
@ -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)
|
76
python/src/trezorlib/cli/stellar.py
Normal file
76
python/src/trezorlib/cli/stellar.py
Normal file
@ -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)
|
68
python/src/trezorlib/cli/tezos.py
Normal file
68
python/src/trezorlib/cli/tezos.py
Normal file
@ -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
110
python/src/trezorlib/cli/webauthn.py
Normal file
110
python/src/trezorlib/cli/webauthn.py
Normal file
@ -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.
|
# 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
|
# 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
|
# 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.
|
# 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>.
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
|
||||||
from . import messages as proto
|
from . import messages as proto
|
||||||
from .tools import expect
|
from .tools import expect
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user