Merge pull request #583 from LedgerHQ/feat/apa/eip712_improvements
EIP-712 filtering improvements
@@ -106,8 +106,20 @@ class EthAppClient:
|
||||
filters_count,
|
||||
sig))
|
||||
|
||||
def eip712_filtering_show_field(self, name: str, sig: bytes):
|
||||
return self._exchange_async(self._cmd_builder.eip712_filtering_show_field(name, sig))
|
||||
def eip712_filtering_amount_join_token(self, token_idx: int, sig: bytes):
|
||||
return self._exchange_async(self._cmd_builder.eip712_filtering_amount_join_token(token_idx,
|
||||
sig))
|
||||
|
||||
def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: bytes):
|
||||
return self._exchange_async(self._cmd_builder.eip712_filtering_amount_join_value(token_idx,
|
||||
name,
|
||||
sig))
|
||||
|
||||
def eip712_filtering_datetime(self, name: str, sig: bytes):
|
||||
return self._exchange_async(self._cmd_builder.eip712_filtering_datetime(name, sig))
|
||||
|
||||
def eip712_filtering_raw(self, name: str, sig: bytes):
|
||||
return self._exchange_async(self._cmd_builder.eip712_filtering_raw(name, sig))
|
||||
|
||||
def sign(self,
|
||||
bip32_path: str,
|
||||
|
||||
@@ -41,8 +41,11 @@ class P2Type(IntEnum):
|
||||
LEGACY_IMPLEM = 0x00
|
||||
NEW_IMPLEM = 0x01
|
||||
FILTERING_ACTIVATE = 0x00
|
||||
FILTERING_CONTRACT_NAME = 0x0f
|
||||
FILTERING_FIELD_NAME = 0xff
|
||||
FILTERING_MESSAGE_INFO = 0x0f
|
||||
FILTERING_DATETIME = 0xfc
|
||||
FILTERING_TOKEN_ADDR_CHECK = 0xfd
|
||||
FILTERING_AMOUNT_FIELD = 0xfe
|
||||
FILTERING_RAW = 0xff
|
||||
|
||||
|
||||
class CommandBuilder:
|
||||
@@ -62,17 +65,11 @@ class CommandBuilder:
|
||||
header.append(len(cdata))
|
||||
return header + cdata
|
||||
|
||||
def _string_to_bytes(self, string: str) -> bytes:
|
||||
data = bytearray()
|
||||
for char in string:
|
||||
data.append(ord(char))
|
||||
return data
|
||||
|
||||
def eip712_send_struct_def_struct_name(self, name: str) -> bytes:
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_DEF,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_NAME,
|
||||
self._string_to_bytes(name))
|
||||
name.encode())
|
||||
|
||||
def eip712_send_struct_def_struct_field(self,
|
||||
field_type: EIP712FieldType,
|
||||
@@ -88,7 +85,7 @@ class CommandBuilder:
|
||||
data.append(typedesc)
|
||||
if field_type == EIP712FieldType.CUSTOM:
|
||||
data.append(len(type_name))
|
||||
data += self._string_to_bytes(type_name)
|
||||
data += type_name.encode()
|
||||
if type_size is not None:
|
||||
data.append(type_size)
|
||||
if len(array_levels) > 0:
|
||||
@@ -98,7 +95,7 @@ class CommandBuilder:
|
||||
if level is not None:
|
||||
data.append(level)
|
||||
data.append(len(key_name))
|
||||
data += self._string_to_bytes(key_name)
|
||||
data += key_name.encode()
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_DEF,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_FIELD,
|
||||
@@ -108,7 +105,7 @@ class CommandBuilder:
|
||||
return self._serialize(InsType.EIP712_SEND_STRUCT_IMPL,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.STRUCT_NAME,
|
||||
self._string_to_bytes(name))
|
||||
name.encode())
|
||||
|
||||
def eip712_send_struct_impl_array(self, size: int) -> bytes:
|
||||
data = bytearray()
|
||||
@@ -162,7 +159,7 @@ class CommandBuilder:
|
||||
def _eip712_filtering_send_name(self, name: str, sig: bytes) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(len(name))
|
||||
data += self._string_to_bytes(name)
|
||||
data += name.encode()
|
||||
data.append(len(sig))
|
||||
data += sig
|
||||
return data
|
||||
@@ -170,25 +167,53 @@ class CommandBuilder:
|
||||
def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(len(name))
|
||||
data += self._string_to_bytes(name)
|
||||
data += name.encode()
|
||||
data.append(filters_count)
|
||||
data.append(len(sig))
|
||||
data += sig
|
||||
return self._serialize(InsType.EIP712_SEND_FILTERING,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.FILTERING_CONTRACT_NAME,
|
||||
P2Type.FILTERING_MESSAGE_INFO,
|
||||
data)
|
||||
|
||||
def eip712_filtering_show_field(self, name: str, sig: bytes) -> bytes:
|
||||
def eip712_filtering_amount_join_token(self, token_idx: int, sig: bytes) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(token_idx)
|
||||
data.append(len(sig))
|
||||
data += sig
|
||||
return self._serialize(InsType.EIP712_SEND_FILTERING,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.FILTERING_FIELD_NAME,
|
||||
P2Type.FILTERING_TOKEN_ADDR_CHECK,
|
||||
data)
|
||||
|
||||
def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: bytes) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(len(name))
|
||||
data += name.encode()
|
||||
data.append(token_idx)
|
||||
data.append(len(sig))
|
||||
data += sig
|
||||
return self._serialize(InsType.EIP712_SEND_FILTERING,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.FILTERING_AMOUNT_FIELD,
|
||||
data)
|
||||
|
||||
def eip712_filtering_datetime(self, name: str, sig: bytes) -> bytes:
|
||||
return self._serialize(InsType.EIP712_SEND_FILTERING,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.FILTERING_DATETIME,
|
||||
self._eip712_filtering_send_name(name, sig))
|
||||
|
||||
def eip712_filtering_raw(self, name: str, sig: bytes) -> bytes:
|
||||
return self._serialize(InsType.EIP712_SEND_FILTERING,
|
||||
P1Type.COMPLETE_SEND,
|
||||
P2Type.FILTERING_RAW,
|
||||
self._eip712_filtering_send_name(name, sig))
|
||||
|
||||
def set_external_plugin(self, plugin_name: str, contract_address: bytes, selector: bytes, sig: bytes) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(len(plugin_name))
|
||||
data += self._string_to_bytes(plugin_name)
|
||||
data += plugin_name.encode()
|
||||
data += contract_address
|
||||
data += selector
|
||||
data += sig
|
||||
|
||||
@@ -165,10 +165,7 @@ def encode_bool(value: str, typesize: int) -> bytes:
|
||||
|
||||
|
||||
def encode_string(value: str, typesize: int) -> bytes:
|
||||
data = bytearray()
|
||||
for char in value:
|
||||
data.append(ord(char))
|
||||
return data
|
||||
return value.encode()
|
||||
|
||||
|
||||
def encode_bytes_fix(value: str, typesize: int) -> bytes:
|
||||
@@ -203,7 +200,17 @@ def send_struct_impl_field(value, field):
|
||||
if filtering_paths:
|
||||
path = ".".join(current_path)
|
||||
if path in filtering_paths.keys():
|
||||
send_filtering_show_field(filtering_paths[path])
|
||||
if filtering_paths[path]["type"] == "amount_join_token":
|
||||
send_filtering_amount_join_token(filtering_paths[path]["token"])
|
||||
elif filtering_paths[path]["type"] == "amount_join_value":
|
||||
send_filtering_amount_join_value(filtering_paths[path]["token"],
|
||||
filtering_paths[path]["name"])
|
||||
elif filtering_paths[path]["type"] == "datetime":
|
||||
send_filtering_datetime(filtering_paths[path]["name"])
|
||||
elif filtering_paths[path]["type"] == "raw":
|
||||
send_filtering_raw(filtering_paths[path]["name"])
|
||||
else:
|
||||
assert False
|
||||
|
||||
with app_client.eip712_send_struct_impl_struct_field(data):
|
||||
enable_autonext()
|
||||
@@ -254,18 +261,24 @@ def send_struct_impl(structs, data, structname):
|
||||
return True
|
||||
|
||||
|
||||
def start_signature_payload(ctx: dict, magic: int) -> bytearray:
|
||||
to_sign = bytearray()
|
||||
# magic number so that signature for one type of filter can't possibly be
|
||||
# valid for another, defined in APDU specs
|
||||
to_sign.append(magic)
|
||||
to_sign += ctx["chainid"]
|
||||
to_sign += ctx["caddr"]
|
||||
to_sign += ctx["schema_hash"]
|
||||
return to_sign
|
||||
|
||||
|
||||
# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
|
||||
def send_filtering_message_info(display_name: str, filters_count: int):
|
||||
global sig_ctx
|
||||
|
||||
to_sign = bytearray()
|
||||
to_sign.append(183)
|
||||
to_sign += sig_ctx["chainid"]
|
||||
to_sign += sig_ctx["caddr"]
|
||||
to_sign += sig_ctx["schema_hash"]
|
||||
to_sign = start_signature_payload(sig_ctx, 183)
|
||||
to_sign.append(filters_count)
|
||||
for char in display_name:
|
||||
to_sign.append(ord(char))
|
||||
to_sign += display_name.encode()
|
||||
|
||||
sig = keychain.sign_data(keychain.Key.CAL, to_sign)
|
||||
with app_client.eip712_filtering_message_info(display_name, filters_count, sig):
|
||||
@@ -273,23 +286,57 @@ def send_filtering_message_info(display_name: str, filters_count: int):
|
||||
disable_autonext()
|
||||
|
||||
|
||||
# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
|
||||
def send_filtering_show_field(display_name):
|
||||
def send_filtering_amount_join_token(token_idx: int):
|
||||
global sig_ctx
|
||||
|
||||
path_str = ".".join(current_path)
|
||||
|
||||
to_sign = bytearray()
|
||||
to_sign.append(72)
|
||||
to_sign += sig_ctx["chainid"]
|
||||
to_sign += sig_ctx["caddr"]
|
||||
to_sign += sig_ctx["schema_hash"]
|
||||
for char in path_str:
|
||||
to_sign.append(ord(char))
|
||||
for char in display_name:
|
||||
to_sign.append(ord(char))
|
||||
to_sign = start_signature_payload(sig_ctx, 11)
|
||||
to_sign += path_str.encode()
|
||||
to_sign.append(token_idx)
|
||||
sig = keychain.sign_data(keychain.Key.CAL, to_sign)
|
||||
with app_client.eip712_filtering_show_field(display_name, sig):
|
||||
with app_client.eip712_filtering_amount_join_token(token_idx, sig):
|
||||
pass
|
||||
|
||||
|
||||
def send_filtering_amount_join_value(token_idx: int, display_name: str):
|
||||
global sig_ctx
|
||||
|
||||
path_str = ".".join(current_path)
|
||||
|
||||
to_sign = start_signature_payload(sig_ctx, 22)
|
||||
to_sign += path_str.encode()
|
||||
to_sign += display_name.encode()
|
||||
to_sign.append(token_idx)
|
||||
sig = keychain.sign_data(keychain.Key.CAL, to_sign)
|
||||
with app_client.eip712_filtering_amount_join_value(token_idx, display_name, sig):
|
||||
pass
|
||||
|
||||
|
||||
def send_filtering_datetime(display_name: str):
|
||||
global sig_ctx
|
||||
|
||||
path_str = ".".join(current_path)
|
||||
|
||||
to_sign = start_signature_payload(sig_ctx, 33)
|
||||
to_sign += path_str.encode()
|
||||
to_sign += display_name.encode()
|
||||
sig = keychain.sign_data(keychain.Key.CAL, to_sign)
|
||||
with app_client.eip712_filtering_datetime(display_name, sig):
|
||||
pass
|
||||
|
||||
|
||||
# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
|
||||
def send_filtering_raw(display_name):
|
||||
global sig_ctx
|
||||
|
||||
path_str = ".".join(current_path)
|
||||
|
||||
to_sign = start_signature_payload(sig_ctx, 72)
|
||||
to_sign += path_str.encode()
|
||||
to_sign += display_name.encode()
|
||||
sig = keychain.sign_data(keychain.Key.CAL, to_sign)
|
||||
with app_client.eip712_filtering_raw(display_name, sig):
|
||||
pass
|
||||
|
||||
|
||||
@@ -300,6 +347,12 @@ def prepare_filtering(filtr_data, message):
|
||||
filtering_paths = filtr_data["fields"]
|
||||
else:
|
||||
filtering_paths = {}
|
||||
if "tokens" in filtr_data:
|
||||
for token in filtr_data["tokens"]:
|
||||
app_client.provide_token_metadata(token["ticker"],
|
||||
bytes.fromhex(token["addr"][2:]),
|
||||
token["decimals"],
|
||||
token["chain_id"])
|
||||
|
||||
|
||||
def handle_optional_domain_values(domain):
|
||||
|
||||
115
doc/ethapp.adoc
@@ -41,6 +41,10 @@ Application version 1.9.19 - 2022-05-17
|
||||
### 1.10.2
|
||||
- Add domain names support
|
||||
|
||||
### 1.11.0
|
||||
- Add EIP-712 amount & date/time filtering
|
||||
- PROVIDE ERC 20 TOKEN INFORMATION & PROVIDE NFT INFORMATION now send back the index where the asset has been stored
|
||||
|
||||
## About
|
||||
|
||||
This application describes the APDU messages interface to communicate with the Ethereum application.
|
||||
@@ -274,27 +278,31 @@ signed by the following secp256k1 public key 045e6c1020c14dc46442fe89f97c0b68cdb
|
||||
'Command'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
|======================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
|
||||
| E0 | 0A | 00 | 00 | variable | 00
|
||||
|==============================================================================================================================
|
||||
| E0 | 0A | 00 | 00 | variable | 00
|
||||
|======================================================================
|
||||
|
||||
'Input data'
|
||||
|
||||
[width="80%"]
|
||||
|==============================================================================================================================
|
||||
| *Description* | *Length*
|
||||
| Length of ERC 20 ticker | 1
|
||||
| ERC 20 ticker | variable
|
||||
| ERC 20 contract address | 20
|
||||
| Number of decimals (big endian encoded) | 4
|
||||
| Chain ID (big endian encoded) | 4
|
||||
| Token information signature | variable
|
||||
|==============================================================================================================================
|
||||
|=======================================================================
|
||||
| *Description* | *Length*
|
||||
| Length of ERC 20 ticker | 1
|
||||
| ERC 20 ticker | variable
|
||||
| ERC 20 contract address | 20
|
||||
| Number of decimals (big endian encoded) | 4
|
||||
| Chain ID (big endian encoded) | 4
|
||||
| Token information signature | variable
|
||||
|=======================================================================
|
||||
|
||||
'Output data'
|
||||
|
||||
None
|
||||
[width="80%"]
|
||||
|====================================================================
|
||||
| *Description* | *Length*
|
||||
| Asset index where the information has been stored | 1
|
||||
|====================================================================
|
||||
|
||||
|
||||
### SIGN ETH EIP 712
|
||||
@@ -509,7 +517,11 @@ type || version || len(collectionName) || collectionName || address || chainId |
|
||||
|
||||
'Output data'
|
||||
|
||||
None
|
||||
[width="80%"]
|
||||
|====================================================================
|
||||
| *Description* | *Length*
|
||||
| Asset index where the information has been stored | 1
|
||||
|====================================================================
|
||||
|
||||
|
||||
### SET PLUGIN
|
||||
@@ -825,10 +837,34 @@ The signature is computed on :
|
||||
|
||||
183 || chain ID (BE) || contract address || schema hash || filters count || display name
|
||||
|
||||
##### Amount-join token
|
||||
|
||||
##### Show field
|
||||
This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
|
||||
The first byte is used so that a signature of one type cannot be valid as another type.
|
||||
|
||||
These commands should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
|
||||
The signature is computed on :
|
||||
|
||||
11 || chain ID (BE) || contract address || schema hash || field path || token index
|
||||
|
||||
##### Amount-join value
|
||||
|
||||
This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
|
||||
|
||||
The signature is computed on :
|
||||
|
||||
22 || chain ID (BE) || contract address || schema hash || field path || display name || token index
|
||||
|
||||
##### Date / Time
|
||||
|
||||
This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
|
||||
|
||||
The signature is computed on :
|
||||
|
||||
33 || chain ID (BE) || contract address || schema hash || field path || display name
|
||||
|
||||
##### Show raw field
|
||||
|
||||
This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
|
||||
The first byte is used so that a signature of one type cannot be valid as another type.
|
||||
|
||||
The signature is computed on :
|
||||
@@ -843,17 +879,23 @@ _Command_
|
||||
|=========================================================================
|
||||
| *CLA* | *INS* | *P1* | *P2* | *LC* | *Le*
|
||||
| E0 | 1E | 00
|
||||
| 00 : activate
|
||||
| 00 : activation
|
||||
|
||||
0F : message info
|
||||
|
||||
FF : show field
|
||||
FC : date/time
|
||||
|
||||
FD : amount-join token
|
||||
|
||||
FE : amount-join value
|
||||
|
||||
FF : raw field
|
||||
| variable | variable
|
||||
|=========================================================================
|
||||
|
||||
_Input data_
|
||||
|
||||
##### If P2 == activate
|
||||
##### If P2 == activation
|
||||
|
||||
None
|
||||
|
||||
@@ -869,7 +911,40 @@ None
|
||||
| Signature | variable
|
||||
|==========================================
|
||||
|
||||
##### If P2 == show field
|
||||
##### If P2 == date / time
|
||||
|
||||
[width="80%"]
|
||||
|==========================================
|
||||
| *Description* | *Length (byte)*
|
||||
| Display name length | 1
|
||||
| Display name | variable
|
||||
| Signature length | 1
|
||||
| Signature | variable
|
||||
|==========================================
|
||||
|
||||
##### If P2 == amount-join token
|
||||
|
||||
[width="80%"]
|
||||
|==========================================
|
||||
| *Description* | *Length (byte)*
|
||||
| Token index | 1
|
||||
| Signature length | 1
|
||||
| Signature | variable
|
||||
|==========================================
|
||||
|
||||
##### If P2 == amount-join value
|
||||
|
||||
[width="80%"]
|
||||
|==========================================
|
||||
| *Description* | *Length (byte)*
|
||||
| Display name length | 1
|
||||
| Display name | variable
|
||||
| Token index | 1
|
||||
| Signature length | 1
|
||||
| Signature | variable
|
||||
|==========================================
|
||||
|
||||
##### If P2 == show raw field
|
||||
|
||||
[width="80%"]
|
||||
|==========================================
|
||||
|
||||
@@ -179,8 +179,10 @@ void handleProvideErc20TokenInformation(uint8_t p1,
|
||||
}
|
||||
}
|
||||
|
||||
G_io_apdu_buffer[0] = tmpCtx.transactionContext.currentAssetIndex;
|
||||
validate_current_asset_info();
|
||||
THROW(0x9000);
|
||||
U2BE_ENCODE(G_io_apdu_buffer, 1, APDU_RESPONSE_OK);
|
||||
io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 3);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -199,8 +199,10 @@ void handleProvideNFTInformation(uint8_t p1,
|
||||
#endif
|
||||
}
|
||||
|
||||
G_io_apdu_buffer[0] = tmpCtx.transactionContext.currentAssetIndex;
|
||||
validate_current_asset_info();
|
||||
THROW(0x9000);
|
||||
U2BE_ENCODE(G_io_apdu_buffer, 1, APDU_RESPONSE_OK);
|
||||
io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 3);
|
||||
}
|
||||
|
||||
#endif // HAVE_NFT_SUPPORT
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
#include "filtering.h"
|
||||
#include "common_712.h"
|
||||
#include "common_ui.h" // ui_idle
|
||||
#include "manage_asset_info.h"
|
||||
|
||||
// APDUs P1
|
||||
#define P1_COMPLETE 0x00
|
||||
#define P1_PARTIAL 0xFF
|
||||
|
||||
// APDUs P2
|
||||
#define P2_DEF_NAME 0x00
|
||||
#define P2_DEF_FIELD 0xFF
|
||||
#define P2_IMPL_NAME P2_DEF_NAME
|
||||
#define P2_IMPL_ARRAY 0x0F
|
||||
#define P2_IMPL_FIELD P2_DEF_FIELD
|
||||
#define P2_FILT_ACTIVATE 0x00
|
||||
#define P2_FILT_MESSAGE_INFO 0x0F
|
||||
#define P2_FILT_DATE_TIME 0xFC
|
||||
#define P2_FILT_AMOUNT_JOIN_TOKEN 0xFD
|
||||
#define P2_FILT_AMOUNT_JOIN_VALUE 0xFE
|
||||
#define P2_FILT_RAW_FIELD 0xFF
|
||||
|
||||
/**
|
||||
* Send the response to the previous APDU command
|
||||
@@ -136,39 +154,46 @@ bool handle_eip712_struct_impl(const uint8_t *const apdu_buf) {
|
||||
bool handle_eip712_filtering(const uint8_t *const apdu_buf) {
|
||||
bool ret = true;
|
||||
bool reply_apdu = true;
|
||||
e_filtering_type type;
|
||||
|
||||
if (eip712_context == NULL) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
ret = false;
|
||||
} else {
|
||||
switch (apdu_buf[OFFSET_P2]) {
|
||||
case P2_FILT_ACTIVATE:
|
||||
if (!N_storage.verbose_eip712) {
|
||||
ui_712_set_filtering_mode(EIP712_FILTERING_FULL);
|
||||
ret = compute_schema_hash();
|
||||
}
|
||||
break;
|
||||
case P2_FILT_MESSAGE_INFO:
|
||||
case P2_FILT_SHOW_FIELD:
|
||||
type = (apdu_buf[OFFSET_P2] == P2_FILT_MESSAGE_INFO)
|
||||
? FILTERING_PROVIDE_MESSAGE_INFO
|
||||
: FILTERING_SHOW_FIELD;
|
||||
if (ui_712_get_filtering_mode() == EIP712_FILTERING_FULL) {
|
||||
ret =
|
||||
provide_filtering_info(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC], type);
|
||||
if ((apdu_buf[OFFSET_P2] == P2_FILT_MESSAGE_INFO) && ret) {
|
||||
reply_apdu = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
PRINTF("Unknown P2 0x%x for APDU 0x%x\n",
|
||||
apdu_buf[OFFSET_P2],
|
||||
apdu_buf[OFFSET_INS]);
|
||||
apdu_response_code = APDU_RESPONSE_INVALID_P1_P2;
|
||||
ret = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ((apdu_buf[OFFSET_P2] != P2_FILT_ACTIVATE) &&
|
||||
(ui_712_get_filtering_mode() != EIP712_FILTERING_FULL)) {
|
||||
handle_eip712_return_code(true);
|
||||
return true;
|
||||
}
|
||||
switch (apdu_buf[OFFSET_P2]) {
|
||||
case P2_FILT_ACTIVATE:
|
||||
if (!N_storage.verbose_eip712) {
|
||||
ui_712_set_filtering_mode(EIP712_FILTERING_FULL);
|
||||
ret = compute_schema_hash();
|
||||
}
|
||||
forget_known_assets();
|
||||
break;
|
||||
case P2_FILT_MESSAGE_INFO:
|
||||
ret = filtering_message_info(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]);
|
||||
if (ret) {
|
||||
reply_apdu = false;
|
||||
}
|
||||
break;
|
||||
case P2_FILT_DATE_TIME:
|
||||
ret = filtering_date_time(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]);
|
||||
break;
|
||||
case P2_FILT_AMOUNT_JOIN_TOKEN:
|
||||
ret = filtering_amount_join_token(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]);
|
||||
break;
|
||||
case P2_FILT_AMOUNT_JOIN_VALUE:
|
||||
ret = filtering_amount_join_value(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]);
|
||||
break;
|
||||
case P2_FILT_RAW_FIELD:
|
||||
ret = filtering_raw_field(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]);
|
||||
break;
|
||||
default:
|
||||
PRINTF("Unknown P2 0x%x for APDU 0x%x\n", apdu_buf[OFFSET_P2], apdu_buf[OFFSET_INS]);
|
||||
apdu_response_code = APDU_RESPONSE_INVALID_P1_P2;
|
||||
ret = false;
|
||||
}
|
||||
if (reply_apdu) {
|
||||
handle_eip712_return_code(ret);
|
||||
|
||||
@@ -6,20 +6,6 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// APDUs P1
|
||||
#define P1_COMPLETE 0x00
|
||||
#define P1_PARTIAL 0xFF
|
||||
|
||||
// APDUs P2
|
||||
#define P2_DEF_NAME 0x00
|
||||
#define P2_DEF_FIELD 0xFF
|
||||
#define P2_IMPL_NAME P2_DEF_NAME
|
||||
#define P2_IMPL_ARRAY 0x0F
|
||||
#define P2_IMPL_FIELD P2_DEF_FIELD
|
||||
#define P2_FILT_ACTIVATE 0x00
|
||||
#define P2_FILT_MESSAGE_INFO 0x0F
|
||||
#define P2_FILT_SHOW_FIELD 0xFF
|
||||
|
||||
#define DOMAIN_STRUCT_NAME "EIP712Domain"
|
||||
|
||||
bool handle_eip712_struct_def(const uint8_t *const apdu_buf);
|
||||
|
||||
@@ -11,12 +11,18 @@
|
||||
#include "path.h"
|
||||
#include "ui_logic.h"
|
||||
|
||||
#define FILT_MAGIC_MESSAGE_INFO 183
|
||||
#define FILT_MAGIC_AMOUNT_JOIN_TOKEN 11
|
||||
#define FILT_MAGIC_AMOUNT_JOIN_VALUE 22
|
||||
#define FILT_MAGIC_DATETIME 33
|
||||
#define FILT_MAGIC_RAW_FIELD 72
|
||||
|
||||
/**
|
||||
* Reconstruct the field path and hash it
|
||||
*
|
||||
* @param[in] hash_ctx the hashing context
|
||||
*/
|
||||
static void hash_filtering_path(cx_hash_t *const hash_ctx) {
|
||||
static void hash_filtering_path(cx_hash_t *hash_ctx) {
|
||||
const void *field_ptr;
|
||||
const char *key;
|
||||
uint8_t key_len;
|
||||
@@ -45,68 +51,51 @@ static void hash_filtering_path(cx_hash_t *const hash_ctx) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the provided signature
|
||||
* Begin the hashing for signature verification
|
||||
*
|
||||
* @param[in] dname_length length of provided substitution name
|
||||
* @param[in] dname provided substitution name
|
||||
* @param[in] sig_length provided signature length
|
||||
* @param[in] sig pointer to the provided signature
|
||||
* @param[in] type the type of filtering
|
||||
* @return whether the signature verification worked or not
|
||||
* @param[in] hash_ctx hashing context
|
||||
* @param[in] magic magic number used in the signature
|
||||
* @return \ref true
|
||||
*/
|
||||
static bool verify_filtering_signature(uint8_t dname_length,
|
||||
const char *const dname,
|
||||
uint8_t sig_length,
|
||||
const uint8_t *const sig,
|
||||
e_filtering_type type) {
|
||||
uint8_t hash[INT256_LENGTH];
|
||||
cx_ecfp_public_key_t verifying_key;
|
||||
cx_sha256_t hash_ctx;
|
||||
static bool sig_verif_start(cx_sha256_t *hash_ctx, uint8_t magic) {
|
||||
uint64_t chain_id;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
|
||||
cx_sha256_init(&hash_ctx);
|
||||
cx_sha256_init(hash_ctx);
|
||||
|
||||
// Magic number, makes it so a signature of one type can't be used as another
|
||||
switch (type) {
|
||||
case FILTERING_SHOW_FIELD:
|
||||
hash_byte(FILTERING_MAGIC_STRUCT_FIELD, (cx_hash_t *) &hash_ctx);
|
||||
break;
|
||||
case FILTERING_PROVIDE_MESSAGE_INFO:
|
||||
hash_byte(FILTERING_MAGIC_CONTRACT_NAME, (cx_hash_t *) &hash_ctx);
|
||||
break;
|
||||
default:
|
||||
apdu_response_code = APDU_RESPONSE_INVALID_DATA;
|
||||
PRINTF("Invalid filtering type when verifying signature!\n");
|
||||
return false;
|
||||
}
|
||||
hash_byte(magic, (cx_hash_t *) hash_ctx);
|
||||
|
||||
// Chain ID
|
||||
chain_id = __builtin_bswap64(eip712_context->chain_id);
|
||||
hash_nbytes((uint8_t *) &chain_id, sizeof(chain_id), (cx_hash_t *) &hash_ctx);
|
||||
hash_nbytes((uint8_t *) &chain_id, sizeof(chain_id), (cx_hash_t *) hash_ctx);
|
||||
|
||||
// Contract address
|
||||
hash_nbytes(eip712_context->contract_addr,
|
||||
sizeof(eip712_context->contract_addr),
|
||||
(cx_hash_t *) &hash_ctx);
|
||||
(cx_hash_t *) hash_ctx);
|
||||
|
||||
// Schema hash
|
||||
hash_nbytes(eip712_context->schema_hash,
|
||||
sizeof(eip712_context->schema_hash),
|
||||
(cx_hash_t *) &hash_ctx);
|
||||
(cx_hash_t *) hash_ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == FILTERING_SHOW_FIELD) {
|
||||
hash_filtering_path((cx_hash_t *) &hash_ctx);
|
||||
} else // FILTERING_PROVIDE_MESSAGE_INFO
|
||||
{
|
||||
hash_byte(ui_712_remaining_filters(), (cx_hash_t *) &hash_ctx);
|
||||
}
|
||||
|
||||
// Display name
|
||||
hash_nbytes((uint8_t *) dname, sizeof(char) * dname_length, (cx_hash_t *) &hash_ctx);
|
||||
/**
|
||||
* End the hashing & do the signature verification
|
||||
*
|
||||
* @param[in] hash_ctx hashing context
|
||||
* @param[in] sig signature
|
||||
* @param[in] sig_length signature length
|
||||
* @return whether the signature verification worked or not
|
||||
*/
|
||||
static bool sig_verif_end(cx_sha256_t *hash_ctx, const uint8_t *sig, uint8_t sig_length) {
|
||||
uint8_t hash[INT256_LENGTH];
|
||||
cx_ecfp_public_key_t verifying_key;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
|
||||
// Finalize hash
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH));
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH));
|
||||
|
||||
CX_CHECK(cx_ecfp_init_public_key_no_throw(CX_CURVE_256K1,
|
||||
LEDGER_SIGNATURE_PUBLIC_KEY,
|
||||
@@ -125,65 +114,341 @@ end:
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide filtering information about upcoming struct field
|
||||
* Check if the given token index is valid
|
||||
*
|
||||
* @param[in] payload the raw data received
|
||||
* @param[in] length payload length
|
||||
* @param[in] type the type of filtering
|
||||
* @return if everything went well or not
|
||||
* @param[in] idx token index
|
||||
* @return whether the index is valid or not
|
||||
*/
|
||||
bool provide_filtering_info(const uint8_t *const payload, uint8_t length, e_filtering_type type) {
|
||||
bool ret = false;
|
||||
uint8_t dname_len;
|
||||
const char *dname;
|
||||
static bool check_token_index(uint8_t idx) {
|
||||
if (idx >= MAX_ASSETS) {
|
||||
PRINTF("Error: token index out of range (%u)\n", idx);
|
||||
return false;
|
||||
}
|
||||
if (!tmpCtx.transactionContext.assetSet[idx]) {
|
||||
PRINTF("Error: token not set (%u)\n", idx);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current element's typename matches the expected one
|
||||
*
|
||||
* @param[in] expected the typename we expect
|
||||
* @return whether it is a match or not
|
||||
*/
|
||||
static bool check_typename(const char *expected) {
|
||||
uint8_t typename_len = 0;
|
||||
const char *typename;
|
||||
|
||||
typename = get_struct_field_typename(path_get_field(), &typename_len);
|
||||
if ((typename_len != strlen(expected)) || (strncmp(typename, expected, typename_len) != 0)) {
|
||||
PRINTF("Error: expected field of type \"%s\" but got \"", expected);
|
||||
for (int i = 0; i < typename_len; ++i) PRINTF("%c", typename[i]);
|
||||
PRINTF("\" instead.\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to give the message information
|
||||
*
|
||||
* @param[in] payload the payload to parse
|
||||
* @param[in] length the payload length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
bool filtering_message_info(const uint8_t *payload, uint8_t length) {
|
||||
uint8_t name_len;
|
||||
const char *name;
|
||||
uint8_t filters_count;
|
||||
uint8_t sig_len;
|
||||
const uint8_t *sig;
|
||||
uint8_t offset = 0;
|
||||
|
||||
if (type == FILTERING_PROVIDE_MESSAGE_INFO) {
|
||||
if (path_get_root_type() != ROOT_DOMAIN) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
} else // FILTERING_SHOW_FIELD
|
||||
{
|
||||
if (path_get_root_type() != ROOT_MESSAGE) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
if (path_get_root_type() != ROOT_DOMAIN) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
if (length > 0) {
|
||||
dname_len = payload[offset++];
|
||||
if ((1 + dname_len) < length) {
|
||||
dname = (char *) &payload[offset];
|
||||
offset += dname_len;
|
||||
if (type == FILTERING_PROVIDE_MESSAGE_INFO) {
|
||||
ui_712_set_filters_count(payload[offset++]);
|
||||
}
|
||||
sig_len = payload[offset++];
|
||||
sig = &payload[offset];
|
||||
offset += sig_len;
|
||||
if ((sig_len > 0) && (offset == length)) {
|
||||
if ((ret = verify_filtering_signature(dname_len, dname, sig_len, sig, type))) {
|
||||
if (type == FILTERING_PROVIDE_MESSAGE_INFO) {
|
||||
if (!N_storage.verbose_eip712) {
|
||||
ui_712_set_title("Contract", 8);
|
||||
ui_712_set_value(dname, dname_len);
|
||||
ui_712_redraw_generic_step();
|
||||
}
|
||||
} else // FILTERING_SHOW_FIELD
|
||||
{
|
||||
if (dname_len > 0) // don't substitute for an empty name
|
||||
{
|
||||
ui_712_set_title(dname, dname_len);
|
||||
}
|
||||
ui_712_flag_field(true, dname_len > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing
|
||||
if ((offset + sizeof(name_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
return ret;
|
||||
name_len = payload[offset++];
|
||||
if ((offset + name_len) > length) {
|
||||
return false;
|
||||
}
|
||||
name = (char *) &payload[offset];
|
||||
offset += name_len;
|
||||
if ((offset + sizeof(filters_count)) > length) {
|
||||
return false;
|
||||
}
|
||||
filters_count = payload[offset++];
|
||||
if ((offset + sizeof(sig_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
sig_len = payload[offset++];
|
||||
if ((offset + sig_len) != length) {
|
||||
return false;
|
||||
}
|
||||
sig = &payload[offset];
|
||||
|
||||
// Verification
|
||||
cx_sha256_t hash_ctx;
|
||||
if (!sig_verif_start(&hash_ctx, FILT_MAGIC_MESSAGE_INFO)) {
|
||||
return false;
|
||||
}
|
||||
hash_byte(filters_count, (cx_hash_t *) &hash_ctx);
|
||||
hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx);
|
||||
if (!sig_verif_end(&hash_ctx, sig, sig_len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handling
|
||||
ui_712_set_filters_count(filters_count);
|
||||
if (!N_storage.verbose_eip712) {
|
||||
ui_712_set_title("Contract", 8);
|
||||
ui_712_set_value(name, name_len);
|
||||
ui_712_redraw_generic_step();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to display a field as a date-time
|
||||
*
|
||||
* @param[in] payload the payload to parse
|
||||
* @param[in] length the payload length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
bool filtering_date_time(const uint8_t *payload, uint8_t length) {
|
||||
uint8_t name_len;
|
||||
const char *name;
|
||||
uint8_t sig_len;
|
||||
const uint8_t *sig;
|
||||
uint8_t offset = 0;
|
||||
|
||||
if (path_get_root_type() != ROOT_MESSAGE) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parsing
|
||||
if ((offset + sizeof(name_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
name_len = payload[offset++];
|
||||
if ((offset + name_len) > length) {
|
||||
return false;
|
||||
}
|
||||
name = (char *) &payload[offset];
|
||||
offset += name_len;
|
||||
if ((offset + sizeof(sig_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
sig_len = payload[offset++];
|
||||
if ((offset + sig_len) != length) {
|
||||
return false;
|
||||
}
|
||||
sig = &payload[offset];
|
||||
|
||||
// Verification
|
||||
cx_sha256_t hash_ctx;
|
||||
if (!sig_verif_start(&hash_ctx, FILT_MAGIC_DATETIME)) {
|
||||
return false;
|
||||
}
|
||||
hash_filtering_path((cx_hash_t *) &hash_ctx);
|
||||
hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx);
|
||||
if (!sig_verif_end(&hash_ctx, sig, sig_len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handling
|
||||
if (!check_typename("uint")) {
|
||||
return false;
|
||||
}
|
||||
if (name_len > 0) { // don't substitute for an empty name
|
||||
ui_712_set_title(name, name_len);
|
||||
}
|
||||
ui_712_flag_field(true, name_len > 0, false, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to display a field as an amount-join (token part)
|
||||
*
|
||||
* @param[in] payload the payload to parse
|
||||
* @param[in] length the payload length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
bool filtering_amount_join_token(const uint8_t *payload, uint8_t length) {
|
||||
uint8_t token_idx;
|
||||
uint8_t sig_len;
|
||||
const uint8_t *sig;
|
||||
uint8_t offset = 0;
|
||||
|
||||
if (path_get_root_type() != ROOT_MESSAGE) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parsing
|
||||
if ((offset + sizeof(token_idx)) > length) {
|
||||
return false;
|
||||
}
|
||||
token_idx = payload[offset++];
|
||||
if ((offset + sizeof(sig_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
sig_len = payload[offset++];
|
||||
if ((offset + sig_len) != length) {
|
||||
return false;
|
||||
}
|
||||
sig = &payload[offset];
|
||||
|
||||
// Verification
|
||||
cx_sha256_t hash_ctx;
|
||||
if (!sig_verif_start(&hash_ctx, FILT_MAGIC_AMOUNT_JOIN_TOKEN)) {
|
||||
return false;
|
||||
}
|
||||
hash_filtering_path((cx_hash_t *) &hash_ctx);
|
||||
hash_byte(token_idx, (cx_hash_t *) &hash_ctx);
|
||||
if (!sig_verif_end(&hash_ctx, sig, sig_len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handling
|
||||
if (!check_typename("address") || !check_token_index(token_idx)) {
|
||||
return false;
|
||||
}
|
||||
ui_712_flag_field(false, false, true, false);
|
||||
ui_712_token_join_prepare_addr_check(token_idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to display a field as an amount-join (value part)
|
||||
*
|
||||
* @param[in] payload the payload to parse
|
||||
* @param[in] length the payload length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
bool filtering_amount_join_value(const uint8_t *payload, uint8_t length) {
|
||||
uint8_t name_len;
|
||||
const char *name;
|
||||
uint8_t token_idx;
|
||||
uint8_t sig_len;
|
||||
const uint8_t *sig;
|
||||
uint8_t offset = 0;
|
||||
|
||||
if (path_get_root_type() != ROOT_MESSAGE) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parsing
|
||||
if ((offset + sizeof(name_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
name_len = payload[offset++];
|
||||
if ((offset + name_len) > length) {
|
||||
return false;
|
||||
}
|
||||
if (name_len == 0) {
|
||||
return false;
|
||||
}
|
||||
name = (char *) &payload[offset];
|
||||
offset += name_len;
|
||||
if ((offset + sizeof(token_idx)) > length) {
|
||||
return false;
|
||||
}
|
||||
token_idx = payload[offset++];
|
||||
if ((offset + sizeof(sig_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
sig_len = payload[offset++];
|
||||
if ((offset + sig_len) != length) {
|
||||
return false;
|
||||
}
|
||||
sig = &payload[offset];
|
||||
|
||||
// Verification
|
||||
cx_sha256_t hash_ctx;
|
||||
if (!sig_verif_start(&hash_ctx, FILT_MAGIC_AMOUNT_JOIN_VALUE)) {
|
||||
return false;
|
||||
}
|
||||
hash_filtering_path((cx_hash_t *) &hash_ctx);
|
||||
hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx);
|
||||
hash_byte(token_idx, (cx_hash_t *) &hash_ctx);
|
||||
if (!sig_verif_end(&hash_ctx, sig, sig_len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handling
|
||||
if (!check_typename("uint") || !check_token_index(token_idx)) {
|
||||
return false;
|
||||
}
|
||||
ui_712_flag_field(false, false, true, false);
|
||||
ui_712_token_join_prepare_amount(token_idx, name, name_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to display a field raw (without formatting)
|
||||
*
|
||||
* @param[in] payload the payload to parse
|
||||
* @param[in] length the payload length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
bool filtering_raw_field(const uint8_t *payload, uint8_t length) {
|
||||
uint8_t name_len;
|
||||
const char *name;
|
||||
uint8_t sig_len;
|
||||
const uint8_t *sig;
|
||||
uint8_t offset = 0;
|
||||
|
||||
if (path_get_root_type() != ROOT_MESSAGE) {
|
||||
apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parsing
|
||||
if ((offset + sizeof(name_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
name_len = payload[offset++];
|
||||
if ((offset + name_len) > length) {
|
||||
return false;
|
||||
}
|
||||
name = (char *) &payload[offset];
|
||||
offset += name_len;
|
||||
if ((offset + sizeof(sig_len)) > length) {
|
||||
return false;
|
||||
}
|
||||
sig_len = payload[offset++];
|
||||
if ((offset + sig_len) != length) {
|
||||
return false;
|
||||
}
|
||||
sig = &payload[offset];
|
||||
|
||||
// Verification
|
||||
cx_sha256_t hash_ctx;
|
||||
if (!sig_verif_start(&hash_ctx, FILT_MAGIC_RAW_FIELD)) {
|
||||
return false;
|
||||
}
|
||||
hash_filtering_path((cx_hash_t *) &hash_ctx);
|
||||
hash_nbytes((uint8_t *) name, sizeof(char) * name_len, (cx_hash_t *) &hash_ctx);
|
||||
if (!sig_verif_end(&hash_ctx, sig, sig_len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handling
|
||||
if (name_len > 0) { // don't substitute for an empty name
|
||||
ui_712_set_title(name, name_len);
|
||||
}
|
||||
ui_712_flag_field(true, name_len > 0, false, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // HAVE_EIP712_FULL_SUPPORT
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define FILTERING_MAGIC_CONTRACT_NAME 0b10110111 // 183
|
||||
#define FILTERING_MAGIC_STRUCT_FIELD 0b01001000 // ~183 = 72
|
||||
|
||||
typedef enum { FILTERING_PROVIDE_MESSAGE_INFO, FILTERING_SHOW_FIELD } e_filtering_type;
|
||||
|
||||
bool provide_filtering_info(const uint8_t *const payload, uint8_t length, e_filtering_type type);
|
||||
bool filtering_message_info(const uint8_t *payload, uint8_t length);
|
||||
bool filtering_date_time(const uint8_t *payload, uint8_t length);
|
||||
bool filtering_amount_join_token(const uint8_t *payload, uint8_t length);
|
||||
bool filtering_amount_join_value(const uint8_t *payload, uint8_t length);
|
||||
bool filtering_raw_field(const uint8_t *payload, uint8_t length);
|
||||
|
||||
#endif // HAVE_EIP712_FULL_SUPPORT
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include "ui_logic.h"
|
||||
#include "mem.h"
|
||||
#include "mem_utils.h"
|
||||
@@ -15,11 +16,48 @@
|
||||
#include "apdu_constants.h" // APDU response codes
|
||||
#include "typed_data.h"
|
||||
#include "commands_712.h"
|
||||
#include "manage_asset_info.h"
|
||||
#include "common_ui.h"
|
||||
#include "domain_name.h"
|
||||
#include "uint_common.h"
|
||||
|
||||
#define AMOUNT_JOIN_FLAG_TOKEN (1 << 0)
|
||||
#define AMOUNT_JOIN_FLAG_VALUE (1 << 1)
|
||||
|
||||
typedef struct {
|
||||
// display name, not NULL-terminated
|
||||
char name[25];
|
||||
uint8_t name_length;
|
||||
uint8_t value[INT256_LENGTH];
|
||||
uint8_t value_length;
|
||||
// indicates the steps the token join has gone through
|
||||
uint8_t flags;
|
||||
} s_amount_join;
|
||||
|
||||
typedef enum {
|
||||
AMOUNT_JOIN_STATE_TOKEN,
|
||||
AMOUNT_JOIN_STATE_VALUE,
|
||||
} e_amount_join_state;
|
||||
|
||||
#define UI_712_FIELD_SHOWN (1 << 0)
|
||||
#define UI_712_FIELD_NAME_PROVIDED (1 << 1)
|
||||
#define UI_712_AMOUNT_JOIN (1 << 2)
|
||||
#define UI_712_DATETIME (1 << 3)
|
||||
|
||||
typedef struct {
|
||||
s_amount_join joins[MAX_ASSETS];
|
||||
uint8_t idx;
|
||||
e_amount_join_state state;
|
||||
} s_amount_context;
|
||||
|
||||
typedef struct {
|
||||
bool shown;
|
||||
bool end_reached;
|
||||
uint8_t filtering_mode;
|
||||
uint8_t filters_to_process;
|
||||
uint8_t field_flags;
|
||||
uint8_t structs_to_review;
|
||||
s_amount_context amount;
|
||||
} t_ui_context;
|
||||
|
||||
static t_ui_context *ui_ctx = NULL;
|
||||
|
||||
/**
|
||||
@@ -34,8 +72,7 @@ static bool ui_712_field_shown(void) {
|
||||
if (N_storage.verbose_eip712 || (path_get_root_type() == ROOT_DOMAIN)) {
|
||||
ret = true;
|
||||
}
|
||||
} else // EIP712_FILTERING_FULL
|
||||
{
|
||||
} else { // EIP712_FILTERING_FULL
|
||||
if (ui_ctx->field_flags & UI_712_FIELD_SHOWN) {
|
||||
ret = true;
|
||||
}
|
||||
@@ -105,8 +142,7 @@ void ui_712_set_value(const char *const str, uint8_t length) {
|
||||
* Redraw the dynamic UI step that shows EIP712 information
|
||||
*/
|
||||
void ui_712_redraw_generic_step(void) {
|
||||
if (!ui_ctx->shown) // Initialize if it is not already
|
||||
{
|
||||
if (!ui_ctx->shown) { // Initialize if it is not already
|
||||
ui_712_start();
|
||||
ui_ctx->shown = true;
|
||||
} else {
|
||||
@@ -186,39 +222,6 @@ static void ui_712_format_str(const uint8_t *const data, uint8_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a substitute token ticker for a given address
|
||||
*
|
||||
* @param[in] addr the given address
|
||||
* @return the ticker name if found, \ref NULL otherwise
|
||||
*/
|
||||
static const char *get_address_token_ticker(const uint8_t *addr) {
|
||||
extraInfo_t *extra_info = get_asset_info_by_addr(addr);
|
||||
if (extra_info != NULL) {
|
||||
return extra_info->token.ticker;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a substitute (token ticker or domain name) for a given address
|
||||
*
|
||||
* @param[in] addr the given address
|
||||
* @return the substitute if found, \ref NULL otherwise
|
||||
*/
|
||||
static const char *get_address_substitute(const uint8_t *addr) {
|
||||
const char *str = NULL;
|
||||
|
||||
str = get_address_token_ticker(addr);
|
||||
if (str == NULL) {
|
||||
if (has_domain_name(&eip712_context->chain_id, addr)) {
|
||||
// No handling of the verbose domains setting
|
||||
str = g_domain_name;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a given data as a string representation of an address
|
||||
*
|
||||
@@ -231,19 +234,12 @@ static bool ui_712_format_addr(const uint8_t *const data, uint8_t length) {
|
||||
apdu_response_code = APDU_RESPONSE_INVALID_DATA;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ui_712_field_shown()) {
|
||||
const char *sub;
|
||||
|
||||
if (!N_storage.verbose_eip712 && ((sub = get_address_substitute(data)) != NULL)) {
|
||||
ui_712_set_value(sub, strlen(sub));
|
||||
} else {
|
||||
if (!getEthDisplayableAddress((uint8_t *) data,
|
||||
strings.tmp.tmp,
|
||||
sizeof(strings.tmp.tmp),
|
||||
chainConfig->chainId)) {
|
||||
THROW(APDU_RESPONSE_ERROR_NO_INFO);
|
||||
}
|
||||
if (!getEthDisplayableAddress((uint8_t *) data,
|
||||
strings.tmp.tmp,
|
||||
sizeof(strings.tmp.tmp),
|
||||
chainConfig->chainId)) {
|
||||
THROW(APDU_RESPONSE_ERROR_NO_INFO);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -376,6 +372,98 @@ static void ui_712_format_uint(const uint8_t *const data, uint8_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format given data as an amount with its ticker and value with correct decimals
|
||||
*
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
static bool ui_712_format_amount_join(void) {
|
||||
const tokenDefinition_t *token;
|
||||
token = &tmpCtx.transactionContext.extraInfo[ui_ctx->amount.idx].token;
|
||||
|
||||
if (!amountToString(ui_ctx->amount.joins[ui_ctx->amount.idx].value,
|
||||
ui_ctx->amount.joins[ui_ctx->amount.idx].value_length,
|
||||
token->decimals,
|
||||
token->ticker,
|
||||
strings.tmp.tmp,
|
||||
sizeof(strings.tmp.tmp))) {
|
||||
return false;
|
||||
}
|
||||
ui_ctx->field_flags |= UI_712_FIELD_SHOWN;
|
||||
ui_712_set_title(ui_ctx->amount.joins[ui_ctx->amount.idx].name,
|
||||
ui_ctx->amount.joins[ui_ctx->amount.idx].name_length);
|
||||
explicit_bzero(&ui_ctx->amount.joins[ui_ctx->amount.idx],
|
||||
sizeof(ui_ctx->amount.joins[ui_ctx->amount.idx]));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of the amount-join
|
||||
*
|
||||
* @param[in] data the data that needs formatting
|
||||
* @param[in] length its length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
static bool update_amount_join(const uint8_t *data, uint8_t length) {
|
||||
tokenDefinition_t *token;
|
||||
|
||||
token = &tmpCtx.transactionContext.extraInfo[ui_ctx->amount.idx].token;
|
||||
switch (ui_ctx->amount.state) {
|
||||
case AMOUNT_JOIN_STATE_TOKEN:
|
||||
if (memcmp(data, token->address, ADDRESS_LENGTH) != 0) {
|
||||
return false;
|
||||
}
|
||||
ui_ctx->amount.joins[ui_ctx->amount.idx].flags |= AMOUNT_JOIN_FLAG_TOKEN;
|
||||
break;
|
||||
|
||||
case AMOUNT_JOIN_STATE_VALUE:
|
||||
memcpy(ui_ctx->amount.joins[ui_ctx->amount.idx].value, data, length);
|
||||
ui_ctx->amount.joins[ui_ctx->amount.idx].value_length = length;
|
||||
ui_ctx->amount.joins[ui_ctx->amount.idx].flags |= AMOUNT_JOIN_FLAG_VALUE;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format given data as a human-readable date/time representation
|
||||
*
|
||||
* @param[in] data the data that needs formatting
|
||||
* @param[in] length its length
|
||||
* @return whether it was successful or not
|
||||
*/
|
||||
static bool ui_712_format_datetime(const uint8_t *data, uint8_t length) {
|
||||
struct tm tstruct;
|
||||
int shown_hour;
|
||||
time_t timestamp = u64_from_BE(data, length);
|
||||
|
||||
if (gmtime_r(×tamp, &tstruct) == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (tstruct.tm_hour == 0) {
|
||||
shown_hour = 12;
|
||||
} else {
|
||||
shown_hour = tstruct.tm_hour;
|
||||
if (shown_hour > 12) {
|
||||
shown_hour -= 12;
|
||||
}
|
||||
}
|
||||
snprintf(strings.tmp.tmp,
|
||||
sizeof(strings.tmp.tmp),
|
||||
"%04d-%02d-%02d\n%02d:%02d:%02d %s UTC",
|
||||
tstruct.tm_year + 1900,
|
||||
tstruct.tm_mon + 1,
|
||||
tstruct.tm_mday,
|
||||
shown_hour,
|
||||
tstruct.tm_min,
|
||||
tstruct.tm_sec,
|
||||
(tstruct.tm_hour < 12) ? "AM" : "PM");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to notify of a new field to review in the current struct (key + value)
|
||||
*
|
||||
@@ -434,6 +522,25 @@ bool ui_712_new_field(const void *const field_ptr, const uint8_t *const data, ui
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ui_ctx->field_flags & UI_712_AMOUNT_JOIN) {
|
||||
if (!update_amount_join(data, length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ui_ctx->amount.joins[ui_ctx->amount.idx].flags ==
|
||||
(AMOUNT_JOIN_FLAG_TOKEN | AMOUNT_JOIN_FLAG_VALUE)) {
|
||||
if (!ui_712_format_amount_join()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ui_ctx->field_flags & UI_712_DATETIME) {
|
||||
if (!ui_712_format_datetime(data, length)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this field is supposed to be displayed
|
||||
if (ui_712_field_shown()) {
|
||||
ui_712_redraw_generic_step();
|
||||
@@ -465,6 +572,7 @@ bool ui_712_init(void) {
|
||||
ui_ctx->shown = false;
|
||||
ui_ctx->end_reached = false;
|
||||
ui_ctx->filtering_mode = EIP712_FILTERING_BASIC;
|
||||
explicit_bzero(&ui_ctx->amount, sizeof(ui_ctx->amount));
|
||||
} else {
|
||||
apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY;
|
||||
}
|
||||
@@ -508,14 +616,22 @@ unsigned int ui_712_reject() {
|
||||
*
|
||||
* @param[in] show if this field should be shown on the device
|
||||
* @param[in] name_provided if a substitution name has been provided
|
||||
* @param[in] token_join if this field is part of a token join
|
||||
* @param[in] datetime if this field should be shown and formatted as a date/time
|
||||
*/
|
||||
void ui_712_flag_field(bool show, bool name_provided) {
|
||||
void ui_712_flag_field(bool show, bool name_provided, bool token_join, bool datetime) {
|
||||
if (show) {
|
||||
ui_ctx->field_flags |= UI_712_FIELD_SHOWN;
|
||||
}
|
||||
if (name_provided) {
|
||||
ui_ctx->field_flags |= UI_712_FIELD_NAME_PROVIDED;
|
||||
}
|
||||
if (token_join) {
|
||||
ui_ctx->field_flags |= UI_712_AMOUNT_JOIN;
|
||||
}
|
||||
if (datetime) {
|
||||
ui_ctx->field_flags |= UI_712_DATETIME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,4 +705,18 @@ void ui_712_notify_filter_change(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void ui_712_token_join_prepare_addr_check(uint8_t index) {
|
||||
ui_ctx->amount.idx = index;
|
||||
ui_ctx->amount.state = AMOUNT_JOIN_STATE_TOKEN;
|
||||
}
|
||||
|
||||
void ui_712_token_join_prepare_amount(uint8_t index, const char *name, uint8_t name_length) {
|
||||
uint8_t cpy_len = MIN(sizeof(ui_ctx->amount.joins[index].name), name_length);
|
||||
|
||||
ui_ctx->amount.idx = index;
|
||||
ui_ctx->amount.state = AMOUNT_JOIN_STATE_VALUE;
|
||||
memcpy(ui_ctx->amount.joins[index].name, name, cpy_len);
|
||||
ui_ctx->amount.joins[index].name_length = cpy_len;
|
||||
}
|
||||
|
||||
#endif // HAVE_EIP712_FULL_SUPPORT
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include "ux.h"
|
||||
|
||||
#define UI_712_FIELD_SHOWN (1 << 0)
|
||||
#define UI_712_FIELD_NAME_PROVIDED (1 << 1)
|
||||
#include "uint256.h"
|
||||
|
||||
typedef enum { EIP712_FILTERING_BASIC, EIP712_FILTERING_FULL } e_eip712_filtering_mode;
|
||||
typedef enum {
|
||||
@@ -16,15 +14,6 @@ typedef enum {
|
||||
EIP712_NO_MORE_FIELD
|
||||
} e_eip712_nfs; // next field state
|
||||
|
||||
typedef struct {
|
||||
bool shown;
|
||||
bool end_reached;
|
||||
uint8_t filtering_mode;
|
||||
uint8_t filters_to_process;
|
||||
uint8_t field_flags;
|
||||
uint8_t structs_to_review;
|
||||
} t_ui_context;
|
||||
|
||||
bool ui_712_init(void);
|
||||
void ui_712_deinit(void);
|
||||
e_eip712_nfs ui_712_next_field(void);
|
||||
@@ -37,7 +26,7 @@ void ui_712_set_title(const char *const str, uint8_t length);
|
||||
void ui_712_set_value(const char *const str, uint8_t length);
|
||||
void ui_712_message_hash(void);
|
||||
void ui_712_redraw_generic_step(void);
|
||||
void ui_712_flag_field(bool show, bool name_provided);
|
||||
void ui_712_flag_field(bool show, bool name_provided, bool token_join, bool datetime);
|
||||
void ui_712_field_flags_reset(void);
|
||||
void ui_712_finalize_field(void);
|
||||
void ui_712_set_filtering_mode(e_eip712_filtering_mode mode);
|
||||
@@ -46,6 +35,8 @@ void ui_712_set_filters_count(uint8_t count);
|
||||
uint8_t ui_712_remaining_filters(void);
|
||||
void ui_712_queue_struct_to_review(void);
|
||||
void ui_712_notify_filter_change(void);
|
||||
void ui_712_token_join_prepare_addr_check(uint8_t index);
|
||||
void ui_712_token_join_prepare_amount(uint8_t index, const char *name, uint8_t name_length);
|
||||
|
||||
#endif // HAVE_EIP712_FULL_SUPPORT
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"name": "Test JSON",
|
||||
"fields": {
|
||||
"from.name": "From",
|
||||
"to.name" : "To"
|
||||
"from.name": {
|
||||
"type": "raw",
|
||||
"name": "From"
|
||||
},
|
||||
"to.name": {
|
||||
"type": "raw",
|
||||
"name": "To"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
{
|
||||
"name": "OpenSea",
|
||||
"fields": {
|
||||
"maker": "Maker",
|
||||
"taker": "Taker",
|
||||
"basePrice": "Base Price",
|
||||
"expirationTime": "Expiration Time"
|
||||
"maker": {
|
||||
"type": "raw",
|
||||
"name": "Maker"
|
||||
},
|
||||
"taker": {
|
||||
"type": "raw",
|
||||
"name": "Taker"
|
||||
},
|
||||
"basePrice": {
|
||||
"type": "raw",
|
||||
"name": "Base Price"
|
||||
},
|
||||
"expirationTime": {
|
||||
"type": "raw",
|
||||
"name": "Expiration Time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
{
|
||||
"name": "Depthy Test",
|
||||
"fields": {
|
||||
"contents": "Message",
|
||||
"from.name": "Sender",
|
||||
"to.members.[].name": "Recipient",
|
||||
"attach.list.[].name": "Attachment"
|
||||
"contents": {
|
||||
"type": "raw",
|
||||
"name": "Message"
|
||||
},
|
||||
"from.name": {
|
||||
"type": "raw",
|
||||
"name": "Sender"
|
||||
},
|
||||
"to.members.[].name": {
|
||||
"type": "raw",
|
||||
"name": "Recipient"
|
||||
},
|
||||
"attach.list.[].name": {
|
||||
"type": "raw",
|
||||
"name": "Attachment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"name": "Ethereum sign-in",
|
||||
"fields": {
|
||||
"curDate": "Timestamp",
|
||||
"id": "Identifier"
|
||||
"curDate": {
|
||||
"type": "raw",
|
||||
"name": "Timestamp"
|
||||
},
|
||||
"id": {
|
||||
"type": "raw",
|
||||
"name": "Identifier"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"domain": {
|
||||
"chainId": 1,
|
||||
"name": "Token test",
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
|
||||
"version": "1"
|
||||
},
|
||||
"message": {
|
||||
"from": "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa",
|
||||
"to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"amount": "117",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
},
|
||||
"primaryType": "Transfer",
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Transfer": [
|
||||
{ "name": "from", "type": "address" },
|
||||
{ "name": "to", "type": "address" },
|
||||
{ "name": "amount", "type": "uint256" },
|
||||
{ "name": "token", "type": "address" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 414 B |
|
Before Width: | Height: | Size: 338 B |
|
Before Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 324 B |
|
Before Width: | Height: | Size: 371 B |
|
Before Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 709 B |
|
Before Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 774 B |
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 381 B After Width: | Height: | Size: 381 B |
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 463 B |
|
After Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 298 B |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 655 B |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 736 B |
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 819 B |
|
After Width: | Height: | Size: 420 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 427 B |
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 381 B After Width: | Height: | Size: 381 B |
|
Before Width: | Height: | Size: 414 B |
|
Before Width: | Height: | Size: 338 B |
|
Before Width: | Height: | Size: 336 B |
|
Before Width: | Height: | Size: 324 B |
|
Before Width: | Height: | Size: 371 B |
|
Before Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 709 B |
|
Before Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 774 B |
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 381 B After Width: | Height: | Size: 381 B |
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 463 B |