Files
app-ethereum/tests/speculos/ethereum_client/ethereum_cmd_builder.py
2024-03-27 14:43:15 +01:00

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)