294 lines
8.9 KiB
Python
294 lines
8.9 KiB
Python
import enum
|
|
import logging
|
|
import struct
|
|
from typing import List, Tuple, Union, Iterator, cast
|
|
|
|
from ethereum_client.transaction import EIP712, PersonalTransaction, Transaction
|
|
from ethereum_client.plugin import ERC20Information, Plugin
|
|
from ethereum_client.utils import packed_bip32_path_from_string
|
|
|
|
MAX_APDU_LEN: int = 255
|
|
|
|
def chunked(size, source):
|
|
for i in range(0, len(source), size):
|
|
yield source[i:i+size]
|
|
|
|
def chunkify(data: bytes, chunk_len: int) -> Iterator[Tuple[bool, bytes]]:
|
|
size: int = len(data)
|
|
|
|
if size <= chunk_len:
|
|
yield True, data
|
|
return
|
|
|
|
chunk: int = size // chunk_len
|
|
remaining: int = size % chunk_len
|
|
offset: int = 0
|
|
|
|
for i in range(chunk):
|
|
yield False, data[offset:offset + chunk_len]
|
|
offset += chunk_len
|
|
|
|
if remaining:
|
|
yield True, data[offset:]
|
|
|
|
class InsType(enum.IntEnum):
|
|
INS_GET_PUBLIC_KEY = 0x02
|
|
INS_SIGN_TX = 0x04
|
|
INS_GET_CONFIGURATION = 0x06
|
|
INS_SIGN_PERSONAL_TX = 0x08
|
|
INS_PROVIDE_ERC20 = 0x0A
|
|
INS_SIGN_EIP712 = 0x0c
|
|
INS_ETH2_GET_PUBLIC_KEY = 0x0E
|
|
INS_SET_ETH2_WITHDRAWAL = 0x10
|
|
INS_SET_EXTERNAL_PLUGIN = 0x12
|
|
INS_PROVIDE_NFT_INFORMATION = 0x14
|
|
INS_SET_PLUGIN = 0x16
|
|
INS_PERFORM_PRIVACY_OPERATION = 0x18
|
|
|
|
|
|
class EthereumCommandBuilder:
|
|
"""APDU command builder for the Boilerplate application.
|
|
|
|
Parameters
|
|
----------
|
|
debug: bool
|
|
Whether you want to see logging or not.
|
|
|
|
Attributes
|
|
----------
|
|
debug: bool
|
|
Whether you want to see logging or not.
|
|
|
|
"""
|
|
CLA: int = 0xE0
|
|
|
|
def __init__(self, debug: bool = False):
|
|
"""Init constructor."""
|
|
self.debug = debug
|
|
|
|
def serialize(self,
|
|
cla: int,
|
|
ins: Union[int, enum.IntEnum],
|
|
p1: int = 0,
|
|
p2: int = 0,
|
|
cdata: bytes = b"") -> bytes:
|
|
"""Serialize the whole APDU command (header + data).
|
|
|
|
Parameters
|
|
----------
|
|
cla : int
|
|
Instruction class: CLA (1 byte)
|
|
ins : Union[int, IntEnum]
|
|
Instruction code: INS (1 byte)
|
|
p1 : int
|
|
Instruction parameter 1: P1 (1 byte).
|
|
p2 : int
|
|
Instruction parameter 2: P2 (1 byte).
|
|
cdata : bytes
|
|
Bytes of command data.
|
|
|
|
Returns
|
|
-------
|
|
bytes
|
|
Bytes of a complete APDU command.
|
|
|
|
"""
|
|
ins = cast(int, ins.value) if isinstance(ins, enum.IntEnum) else cast(int, ins)
|
|
|
|
header: bytes = struct.pack("BBBBB",
|
|
cla,
|
|
ins,
|
|
p1,
|
|
p2,
|
|
len(cdata)) # add Lc to APDU header
|
|
|
|
if self.debug:
|
|
logging.info("header: %s", header.hex())
|
|
logging.info("cdata: %s", cdata.hex())
|
|
|
|
return header + cdata
|
|
|
|
def get_configuration(self) -> bytes:
|
|
"""Command builder for GET_CONFIGURATON
|
|
|
|
Returns
|
|
-------
|
|
bytes
|
|
APDU command for GET_CONFIGURATON
|
|
|
|
"""
|
|
return self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_GET_CONFIGURATION,
|
|
p1=0x00,
|
|
p2=0x00,
|
|
cdata=b"")
|
|
|
|
def _same_header_builder(self, data: Union[Plugin, ERC20Information], ins: int) -> bytes:
|
|
return self.serialize(cla=self.CLA,
|
|
ins=ins,
|
|
p1=0x00,
|
|
p2=0x00,
|
|
cdata=data.serialize())
|
|
|
|
def set_plugin(self, plugin: Plugin) -> bytes:
|
|
return self._same_header_builder(plugin, InsType.INS_SET_PLUGIN)
|
|
|
|
def provide_nft_information(self, plugin: Plugin) -> bytes:
|
|
return self._same_header_builder(plugin, InsType.INS_PROVIDE_NFT_INFORMATION)
|
|
|
|
def provide_erc20_token_information(self, info: ERC20Information):
|
|
return self._same_header_builder(info, InsType.INS_PROVIDE_ERC20)
|
|
|
|
def get_public_key(self, bip32_path: str, display: bool = False) -> bytes:
|
|
"""Command builder for GET_PUBLIC_KEY.
|
|
|
|
Parameters
|
|
----------
|
|
bip32_path: str
|
|
String representation of BIP32 path.
|
|
display : bool
|
|
Whether you want to display the address on the device.
|
|
|
|
Returns
|
|
-------
|
|
bytes
|
|
APDU command for GET_PUBLIC_KEY.
|
|
|
|
"""
|
|
cdata = packed_bip32_path_from_string(bip32_path)
|
|
|
|
return self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_GET_PUBLIC_KEY,
|
|
p1=0x01 if display else 0x00,
|
|
p2=0x01,
|
|
cdata=cdata)
|
|
|
|
def perform_privacy_operation(self, bip32_path: str, display: bool, shared_secret: bool) -> bytes:
|
|
"""Command builder for INS_PERFORM_PRIVACY_OPERATION.
|
|
|
|
Parameters
|
|
----------
|
|
bip32_path : str
|
|
String representation of BIP32 path.
|
|
Third party public key on Curve25519 : 32 bytes
|
|
Optional if returning the shared secret
|
|
|
|
"""
|
|
cdata = packed_bip32_path_from_string(bip32_path)
|
|
|
|
return self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_PERFORM_PRIVACY_OPERATION,
|
|
p1=0x01 if display else 0x00,
|
|
p2=0x01 if shared_secret else 0x00,
|
|
cdata=cdata)
|
|
|
|
|
|
def simple_sign_tx(self, bip32_path: str, transaction: Transaction) -> bytes:
|
|
"""Command builder for INS_SIGN_TX.
|
|
|
|
Parameters
|
|
----------
|
|
bip32_path : str
|
|
String representation of BIP32 path.
|
|
transaction : Transaction
|
|
Representation of the transaction to be signed.
|
|
|
|
Yields
|
|
-------
|
|
bytes
|
|
APDU command chunk for INS_SIGN_TX.
|
|
|
|
"""
|
|
cdata = packed_bip32_path_from_string(bip32_path)
|
|
|
|
tx: bytes = transaction.serialize()
|
|
|
|
cdata = cdata + tx
|
|
|
|
return self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_SIGN_TX,
|
|
p1=0x00,
|
|
p2=0x00,
|
|
cdata=cdata)
|
|
|
|
def sign_eip712(self, bip32_path: str, transaction: EIP712) -> bytes:
|
|
"""Command builder for INS_SIGN_EIP712.
|
|
|
|
Parameters
|
|
----------
|
|
bip32_path : str
|
|
String representation of BIP32 path.
|
|
transaction : EIP712
|
|
Domain hash -> 32 bytes
|
|
Message hash -> 32 bytes
|
|
|
|
Yields
|
|
-------
|
|
bytes
|
|
APDU command chunk for INS_SIGN_EIP712.
|
|
|
|
"""
|
|
cdata = packed_bip32_path_from_string(bip32_path)
|
|
|
|
|
|
tx: bytes = transaction.serialize()
|
|
|
|
cdata = cdata + tx
|
|
|
|
return self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_SIGN_EIP712,
|
|
p1=0x00,
|
|
p2=0x00,
|
|
cdata=cdata)
|
|
|
|
def personal_sign_tx(self, bip32_path: str, transaction: PersonalTransaction) -> Tuple[bool,bytes]:
|
|
"""Command builder for INS_SIGN_PERSONAL_TX.
|
|
|
|
Parameters
|
|
----------
|
|
bip32_path : str
|
|
String representation of BIP32 path.
|
|
transaction : Transaction
|
|
Representation of the transaction to be signed.
|
|
|
|
Yields
|
|
-------
|
|
bytes
|
|
APDU command chunk for INS_SIGN_PERSONAL_TX.
|
|
|
|
"""
|
|
|
|
cdata = packed_bip32_path_from_string(bip32_path)
|
|
|
|
tx: bytes = transaction.serialize()
|
|
|
|
cdata = cdata + tx
|
|
last_chunk = len(cdata) // MAX_APDU_LEN
|
|
|
|
# The generator allows to send apdu frames because we can't send an apdu > 255
|
|
for i, (chunk) in enumerate(chunked(MAX_APDU_LEN, cdata)):
|
|
if i == 0 and i == last_chunk:
|
|
yield True, self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_SIGN_PERSONAL_TX,
|
|
p1=0x00,
|
|
p2=0x00,
|
|
cdata=chunk)
|
|
elif i == 0:
|
|
yield False, self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_SIGN_PERSONAL_TX,
|
|
p1=0x00,
|
|
p2=0x00,
|
|
cdata=chunk)
|
|
elif i == last_chunk:
|
|
yield True, self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_SIGN_PERSONAL_TX,
|
|
p1=0x80,
|
|
p2=0x00,
|
|
cdata=chunk)
|
|
else:
|
|
yield False, self.serialize(cla=self.CLA,
|
|
ins=InsType.INS_SIGN_PERSONAL_TX,
|
|
p1=0x80,
|
|
p2=0x00,
|
|
cdata=chunk)
|