diff --git a/apps/hedera_app/hedera_api.c b/apps/hedera_app/hedera_api.c new file mode 100644 index 000000000..182905d9c --- /dev/null +++ b/apps/hedera_app/hedera_api.c @@ -0,0 +1,77 @@ +/** + * @file hedera_api.c + * @author Cypherock X1 Team + * @brief Defines helper APIs for the Hedera app. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#include "hedera_api.h" +#include +#include +#include "common_error.h" +#include "core_api.h" +#include "events.h" + +bool decode_hedera_query(const uint8_t *data, uint16_t data_size, hedera_query_t *query_out) { + if (NULL == data || NULL == query_out || 0 == data_size) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_DECODING_FAILED); + return false; + } + memzero(query_out, sizeof(hedera_query_t)); + pb_istream_t stream = pb_istream_from_buffer(data, data_size); + bool status = pb_decode(&stream, HEDERA_QUERY_FIELDS, query_out); + if (!status) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_DECODING_FAILED); + } + return status; +} + +bool encode_hedera_result(const hedera_result_t *result, uint8_t *buffer, uint16_t max_buffer_len, size_t *bytes_written_out) { + if (NULL == result || NULL == buffer || NULL == bytes_written_out) return false; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, max_buffer_len); + bool status = pb_encode(&stream, HEDERA_RESULT_FIELDS, result); + if (status) { + *bytes_written_out = stream.bytes_written; + } + return status; +} + +bool check_hedera_query(const hedera_query_t *query, pb_size_t exp_query_tag) { + if ((NULL == query) || (exp_query_tag != query->which_request)) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_QUERY); + return false; + } + return true; +} + +hedera_result_t init_hedera_result(pb_size_t result_tag) { + hedera_result_t result = HEDERA_RESULT_INIT_ZERO; + result.which_response = result_tag; + return result; +} + +void hedera_send_error(pb_size_t which_error, uint32_t error_code) { + hedera_result_t result = init_hedera_result(HEDERA_RESULT_COMMON_ERROR_TAG); + result.common_error = init_common_error(which_error, error_code); + hedera_send_result(&result); +} + +void hedera_send_result(const hedera_result_t *result) { + uint8_t buffer[1700] = {0}; + size_t bytes_encoded = 0; + ASSERT(encode_hedera_result(result, buffer, sizeof(buffer), &bytes_encoded)); + send_response_to_host(buffer, bytes_encoded); +} + +bool hedera_get_query(hedera_query_t *query, pb_size_t exp_query_tag) { + evt_status_t event = get_events(EVENT_CONFIG_USB, MAX_INACTIVITY_TIMEOUT); + if (event.p0_event.flag) { + return false; + } + if (!decode_hedera_query(event.usb_event.p_msg, event.usb_event.msg_size, query)) { + return false; + } + if (!check_hedera_query(query, exp_query_tag)) { + return false; + } + return true; +} \ No newline at end of file diff --git a/apps/hedera_app/hedera_api.h b/apps/hedera_app/hedera_api.h new file mode 100644 index 000000000..a7f300f65 --- /dev/null +++ b/apps/hedera_app/hedera_api.h @@ -0,0 +1,34 @@ +/** + * @file hedera_api.h + * @author Cypherock X1 Team + * @brief Header file for Hedera app helper functions. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#ifndef HEDERA_API_H +#define HEDERA_API_H + +#include +#include + +// API to decode query from host with HEDERA_QUERY_FIELDS +bool decode_hedera_query(const uint8_t *data, uint16_t data_size, hedera_query_t *query_out); + +// Encodes the Hedera result with HEDERA_RESULT_FIELDS to byte-stream +bool encode_hedera_result(const hedera_result_t *result, uint8_t *buffer, uint16_t max_buffer_len, size_t *bytes_written_out); + +// Checks if the `which_request` field of the query matches the expected tag. +bool check_hedera_query(const hedera_query_t *query, pb_size_t exp_query_tag); + +// Returns a zero-initialized hedera_result_t with the specified result tag. +hedera_result_t init_hedera_result(pb_size_t result_tag); + +// Sends an error response to the host. +void hedera_send_error(pb_size_t which_error, uint32_t error_code); + +// Encodes and sends a result to the host. +void hedera_send_result(const hedera_result_t *result); + +// Waits for and decodes a query from the host, ensuring it matches the expected tag. +bool hedera_get_query(hedera_query_t *query, pb_size_t exp_query_tag); + +#endif \ No newline at end of file diff --git a/apps/hedera_app/hedera_context.h b/apps/hedera_app/hedera_context.h new file mode 100644 index 000000000..8bfabff54 --- /dev/null +++ b/apps/hedera_app/hedera_context.h @@ -0,0 +1,30 @@ +/** + * @file hedera_context.h + * @author Cypherock X1 Team + * @brief Header file defining typedefs and MACROS for the Hedera app. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#ifndef HEDERA_CONTEXT_H +#define HEDERA_CONTEXT_H + +#include +#include + +#define HEDERA_NAME "Hedera" +#define HEDERA_LUNIT "HBAR" + +#define HEDERA_COIN_DEPTH 5 + +// Derivation path: m/44'/3030'/0'/0'/i' +#define HEDERA_PURPOSE_INDEX (0x80000000 | 44) +#define HEDERA_COIN_INDEX (0x80000000 | 3030) +#define HEDERA_ACCOUNT_INDEX (0x80000000 | 0) +#define HEDERA_CHANGE_INDEX (0x00000000 | 0) + +#define HEDERA_PUB_KEY_SIZE 32 // Raw Ed25519 public key +#define HEDERA_ADDRESS_STRING_SIZE (HEDERA_PUB_KEY_SIZE * 2 + 1) // Hex string + null +#define HEDERA_SIGNATURE_SIZE 64 // Raw Ed25519 signature + +#define MAX_TXN_SIZE 512 // Maximum size of a serialized transaction body + +#endif /* HEDERA_CONTEXT_H */ \ No newline at end of file diff --git a/apps/hedera_app/hedera_helpers.c b/apps/hedera_app/hedera_helpers.c new file mode 100644 index 000000000..bbc28e0e0 --- /dev/null +++ b/apps/hedera_app/hedera_helpers.c @@ -0,0 +1,60 @@ +/** + * @file hedera_helpers.c + * @author Cypherock X1 Team + * @brief Utilities specific to the Hedera app. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#include "hedera_helpers.h" +#include "coin_utils.h" +#include "hedera_context.h" +#include +#include + +bool hedera_derivation_path_guard(const uint32_t *path, uint8_t levels) { + if (levels != HEDERA_COIN_DEPTH) { + return false; + } + + // Path must be m/44'/3030'/0'/0'/i' + return (path[0] == HEDERA_PURPOSE_INDEX && + path[1] == HEDERA_COIN_INDEX && + path[2] == HEDERA_ACCOUNT_INDEX && + path[3] == HEDERA_CHANGE_INDEX && + is_non_hardened(path[4])); +} + +void hedera_format_pubkey(const uint8_t *pubkey, char *out_str) { + byte_array_to_hex_string(pubkey, HEDERA_PUB_KEY_SIZE, out_str, HEDERA_ADDRESS_STRING_SIZE); +} + +void hedera_format_account_id(const Hedera_AccountID *account_id, char *out_str) { + snprintf(out_str, 40, "%lld.%lld.%lld", + (long long)account_id->shardNum, + (long long)account_id->realmNum, + (long long)account_id->account.accountNum); +} + +void hedera_format_tinybars_to_hbar_string(int64_t tinybars, char *out_str) { + char temp_str[30]; + const int64_t hbar_div = 100000000; + + int sign = (tinybars < 0) ? -1 : 1; + if (tinybars < 0) tinybars = -tinybars; + + int64_t whole_part = tinybars / hbar_div; + int64_t frac_part = tinybars % hbar_div; + + // Format fractional part with leading zeros + snprintf(temp_str, sizeof(temp_str), "%lld.%08lld", (long long)whole_part, (long long)frac_part); + + // Trim trailing zeros + char *end = temp_str + strlen(temp_str) - 1; + while (end > temp_str && *end == '0') { + *end-- = '\0'; + } + if (end > temp_str && *end == '.') { + *end = '\0'; + } + + snprintf(out_str, 40, "%s%s %s", (sign == -1) ? "-" : "", temp_str, HEDERA_LUNIT); +} \ No newline at end of file diff --git a/apps/hedera_app/hedera_helpers.h b/apps/hedera_app/hedera_helpers.h new file mode 100644 index 000000000..8608036ab --- /dev/null +++ b/apps/hedera_app/hedera_helpers.h @@ -0,0 +1,27 @@ +/** + * @file hedera_helpers.h + * @author Cypherock X1 Team + * @brief Utilities API definitions for the Hedera app. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#ifndef HEDERA_HELPERS_H +#define HEDERA_HELPERS_H + +#include +#include +#include +#include "proto/basic_types.pb.h" + +// Verifies the derivation path for Hedera (m/44'/3030'/0'/0'/i'). +bool hedera_derivation_path_guard(const uint32_t *path, uint8_t levels); + +// Formats a raw public key into a hex string for display. +void hedera_format_pubkey(const uint8_t *pubkey, char *out_str); + +// Formats an AccountID protobuf struct into a human-readable string "shard.realm.num". +void hedera_format_account_id(const Hedera_AccountID *account_id, char *out_str); + +// Formats a tinybar amount into an HBAR string with 8 decimal places. +void hedera_format_tinybars_to_hbar_string(int64_t tinybars, char *out_str); + +#endif // HEDERA_HELPERS_H \ No newline at end of file diff --git a/apps/hedera_app/hedera_main.c b/apps/hedera_app/hedera_main.c new file mode 100644 index 000000000..936b1bb14 --- /dev/null +++ b/apps/hedera_app/hedera_main.c @@ -0,0 +1,49 @@ +/** + * @file hedera_main.c + * @author Cypherock X1 Team + * @brief A common entry point to various Hedera coin actions. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#include "hedera_main.h" +#include "hedera_api.h" +#include "hedera_priv.h" +#include "status_api.h" + +void hedera_main(usb_event_t usb_evt, const void *hedera_app_config); + +static const cy_app_desc_t hedera_app_desc = { + .id = 24, // IMPORTANT: Use an unused app ID here. + .version = {.major = 1, .minor = 0, .patch = 0}, + .app = hedera_main, + .app_config = NULL +}; + +void hedera_main(usb_event_t usb_evt, const void *hedera_app_config) { + hedera_query_t query = HEDERA_QUERY_INIT_DEFAULT; + + if (!decode_hedera_query(usb_evt.p_msg, usb_evt.msg_size, &query)) { + return; + } + + core_status_set_idle_state(CORE_DEVICE_IDLE_STATE_USB); + + switch ((uint8_t)query.which_request) { + case HEDERA_QUERY_GET_PUBLIC_KEYS_TAG: + case HEDERA_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG: { + hedera_get_pub_keys(&query); + break; + } + case HEDERA_QUERY_SIGN_TXN_TAG: { + hedera_sign_transaction(&query); + break; + } + default: { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_QUERY); + break; + } + } +} + +const cy_app_desc_t *get_hedera_app_desc() { + return &hedera_app_desc; +} \ No newline at end of file diff --git a/apps/hedera_app/hedera_main.h b/apps/hedera_app/hedera_main.h new file mode 100644 index 000000000..ec27d75a7 --- /dev/null +++ b/apps/hedera_app/hedera_main.h @@ -0,0 +1,16 @@ +/** + * @file hedera_main.h + * @author Cypherock X1 Team + * @brief Header for the main entry point of the Hedera app. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#ifndef HEDERA_MAIN_H +#define HEDERA_MAIN_H + +#include "app_registry.h" +#include "events.h" + +// Returns the config for the Hedera app descriptor. +const cy_app_desc_t *get_hedera_app_desc(); + +#endif /* HEDERA_MAIN_H */ \ No newline at end of file diff --git a/apps/hedera_app/hedera_priv.h b/apps/hedera_app/hedera_priv.h new file mode 100644 index 000000000..02277fe2e --- /dev/null +++ b/apps/hedera_app/hedera_priv.h @@ -0,0 +1,37 @@ +/** + * @file hedera_priv.h + * @author Cypherock X1 Team + * @brief Support for Hedera app internal operations. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#ifndef HEDERA_PRIV_H +#define HEDERA_PRIV_H + +#include "hedera/core.pb.h" +#include "hedera/sign_txn.pb.h" +#include "proto/transaction_body.pb.h" // Nanopb generated header +#include "hedera_context.h" + +// Context for the transaction signing flow +typedef struct { + // The structure holds the wallet information of the transaction. + hedera_sign_txn_initiate_request_t init_info; + + // Decoded protobuf transaction body for UI display + Hedera_TransactionBody txn; + + // Raw serialized transaction bytes received from host. This is what we sign. + uint8_t raw_txn_bytes[MAX_TXN_SIZE]; + size_t raw_txn_len; + +} hedera_txn_context_t; + +/* --- FUNCTION PROTOTYPES --- */ + +// Handler for public key derivation flows +void hedera_get_pub_keys(hedera_query_t *query); + +// Handler for transaction signing flows +void hedera_sign_transaction(hedera_query_t *query); + +#endif /* HEDERA_PRIV_H */ \ No newline at end of file diff --git a/apps/hedera_app/hedera_pub_key.c b/apps/hedera_app/hedera_pub_key.c new file mode 100644 index 000000000..68dd969fd --- /dev/null +++ b/apps/hedera_app/hedera_pub_key.c @@ -0,0 +1,161 @@ +/** + * @file hedera_pub_key.c + * @author Cypherock X1 Team + * @brief Generates public key for Hedera derivations. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#include "hedera_priv.h" +#include "hedera_api.h" +#include "hedera_helpers.h" +#include "hedera_context.h" + +#include "wallet_list.h" +#include "ui_screens.h" +#include "ui_core_confirm.h" +#include "reconstruct_wallet_flow.h" +#include "bip32.h" +#include "curves.h" +#include "ed25519.h" +#include "coin_utils.h" + +static bool check_which_request(const hedera_query_t *query, pb_size_t which_request) { + if (which_request != query->get_public_keys.which_request) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_REQUEST); + return false; + } + return true; +} + +static bool validate_request(const hedera_get_public_keys_intiate_request_t *req, const pb_size_t which_request) { + if (0 == req->derivation_paths_count) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_DATA); + return false; + } + + if (HEDERA_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG == which_request && 1 < req->derivation_paths_count) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_DATA); + return false; + } + + for (pb_size_t i = 0; i < req->derivation_paths_count; i++) { + if (!hedera_derivation_path_guard(req->derivation_paths[i].path, req->derivation_paths[i].path_count)) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_DATA); + return false; + } + } + return true; +} + +static bool get_public_key(const uint8_t *seed, const uint32_t *path, uint32_t path_length, uint8_t *public_key) { + HDNode node = {0}; + if (!derive_hdnode_from_path(path, path_length, ED25519_NAME, seed, &node)) { + hedera_send_error(ERROR_COMMON_ERROR_UNKNOWN_ERROR_TAG, 1); + memzero(&node, sizeof(HDNode)); + return false; + } + memcpy(public_key, node.public_key, HEDERA_PUB_KEY_SIZE); + memzero(&node, sizeof(HDNode)); + return true; +} + +static bool fill_public_keys(const hedera_get_public_keys_derivation_path_t *path, const uint8_t *seed, uint8_t public_key_list[][HEDERA_PUB_KEY_SIZE], pb_size_t count) { + for (pb_size_t i = 0; i < count; i++) { + if (!get_public_key(seed, path[i].path, path[i].path_count, public_key_list[i])) { + return false; + } + } + return true; +} + +static bool send_public_keys(hedera_query_t *query, const uint8_t pubkey_list[][HEDERA_PUB_KEY_SIZE], const pb_size_t count, const pb_size_t which_request, const pb_size_t which_response) { + hedera_result_t response = init_hedera_result(which_response); + hedera_get_public_keys_result_response_t *result = &response.get_public_keys.result; + size_t batch_limit = sizeof(result->public_keys) / HEDERA_PUB_KEY_SIZE; + size_t remaining = count; + + response.get_public_keys.which_response = HEDERA_GET_PUBLIC_KEYS_RESPONSE_RESULT_TAG; + while (true) { + size_t batch_size = CY_MIN(batch_limit, remaining); + result->public_keys_count = batch_size; + memcpy(result->public_keys, &pubkey_list[count - remaining], batch_size * HEDERA_PUB_KEY_SIZE); + hedera_send_result(&response); + remaining -= batch_size; + if (0 == remaining) break; + if (!hedera_get_query(query, which_request) || !check_which_request(query, HEDERA_GET_PUBLIC_KEYS_REQUEST_FETCH_NEXT_TAG)) { + return false; + } + } + return true; +} + +static bool get_user_consent(const pb_size_t which_request, const char *wallet_name) { + char msg[100] = ""; + if (HEDERA_QUERY_GET_PUBLIC_KEYS_TAG == which_request) { + snprintf(msg, sizeof(msg), UI_TEXT_ADD_ACCOUNT_PROMPT, HEDERA_NAME, wallet_name); + } else { + snprintf(msg, sizeof(msg), "Export public key for %s?", HEDERA_NAME); + } + return core_confirmation(msg, hedera_send_error); +} + +void hedera_get_pub_keys(hedera_query_t *query) { + char wallet_name[NAME_SIZE] = ""; + uint8_t seed[64] = {0}; + const pb_size_t which_request = query->which_request; + const hedera_get_public_keys_intiate_request_t *init_req; + pb_size_t which_response; + + if (HEDERA_QUERY_GET_PUBLIC_KEYS_TAG == which_request) { + which_response = HEDERA_RESULT_GET_PUBLIC_KEYS_TAG; + init_req = &query->get_public_keys.initiate; + } else { + which_response = HEDERA_RESULT_GET_USER_VERIFIED_PUBLIC_KEY_TAG; + init_req = &query->get_user_verified_public_key.initiate; + } + + const pb_size_t count = init_req->derivation_paths_count; + uint8_t pubkey_list[sizeof(init_req->derivation_paths) / sizeof(hedera_get_public_keys_derivation_path_t)][HEDERA_PUB_KEY_SIZE] = {0}; + + if (!check_which_request(query, HEDERA_GET_PUBLIC_KEYS_REQUEST_INITIATE_TAG) || + !validate_request(init_req, which_request) || + !get_wallet_name_by_id(init_req->wallet_id, (uint8_t *)wallet_name, hedera_send_error)) { + return; + } + + if (!get_user_consent(which_request, wallet_name)) { + return; + } + + set_app_flow_status(HEDERA_GET_PUBLIC_KEYS_STATUS_CONFIRM); + + if (!reconstruct_seed(init_req->wallet_id, &seed[0], hedera_send_error)) { + memzero(seed, sizeof(seed)); + return; + } + + set_app_flow_status(HEDERA_GET_PUBLIC_KEYS_STATUS_SEED_GENERATED); + delay_scr_init(ui_text_processing, DELAY_SHORT); + + bool result = fill_public_keys(init_req->derivation_paths, seed, pubkey_list, count); + memzero(seed, sizeof(seed)); + + if (!result) { + hedera_send_error(ERROR_COMMON_ERROR_UNKNOWN_ERROR_TAG, 1); + return; + } + + if (HEDERA_QUERY_GET_USER_VERIFIED_PUBLIC_KEY_TAG == which_request) { + char pubkey_hex[HEDERA_ADDRESS_STRING_SIZE]; + hedera_format_pubkey(pubkey_list[0], pubkey_hex); + if (!core_scroll_page("Verify Public Key", pubkey_hex, hedera_send_error)) { + return; + } + set_app_flow_status(HEDERA_GET_PUBLIC_KEYS_STATUS_VERIFY); + } + + if (!send_public_keys(query, pubkey_list, count, which_request, which_response)) { + return; + } + + delay_scr_init(ui_text_check_software_wallet_app, DELAY_TIME); +} \ No newline at end of file diff --git a/apps/hedera_app/hedera_txn.c b/apps/hedera_app/hedera_txn.c new file mode 100644 index 000000000..7599b4084 --- /dev/null +++ b/apps/hedera_app/hedera_txn.c @@ -0,0 +1,211 @@ +/** + * @file hedera_txn.c + * @author Cypherock X1 Team + * @brief Source file to handle transaction signing for the Hedera app. + * @copyright Copyright (c) 2025 HODL TECH PTE LTD + */ +#include "hedera_priv.h" +#include "hedera_api.h" +#include "hedera_helpers.h" +#include "hedera_context.h" + +#include "wallet_list.h" +#include "ui_screens.h" +#include "ui_core_confirm.h" +#include "reconstruct_wallet_flow.h" +#include "bip32.h" +#include "curves.h" +#include "ed25519.h" +#include "coin_utils.h" + +#include +#include +#include "proto/transaction_body.pb.h" + +static hedera_txn_context_t *hedera_txn_context = NULL; + +static bool check_which_request(const hedera_query_t *query, pb_size_t which_request) { + if (which_request != query->sign_txn.which_request) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_REQUEST); + return false; + } + return true; +} + +static void send_response(const pb_size_t which_response) { + hedera_result_t result = init_hedera_result(HEDERA_RESULT_SIGN_TXN_TAG); + result.sign_txn.which_response = which_response; + hedera_send_result(&result); +} + +static bool validate_request_data(const hedera_sign_txn_request_t *request) { + if (!hedera_derivation_path_guard(request->initiate.derivation_path, request->initiate.derivation_path_count)) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_DATA); + return false; + } + return true; +} + +static bool handle_initiate_query(const hedera_query_t *query) { + char wallet_name[NAME_SIZE] = ""; + char msg[100] = ""; + + if (!check_which_request(query, HEDERA_SIGN_TXN_REQUEST_INITIATE_TAG) || + !validate_request_data(&query->sign_txn) || + !get_wallet_name_by_id(query->sign_txn.initiate.wallet_id, (uint8_t *)wallet_name, hedera_send_error)) { + return false; + } + + snprintf(msg, sizeof(msg), UI_TEXT_SIGN_TXN_PROMPT, HEDERA_NAME, wallet_name); + if (!core_confirmation(msg, hedera_send_error)) { + return false; + } + + set_app_flow_status(HEDERA_SIGN_TXN_STATUS_CONFIRM); + memcpy(&hedera_txn_context->init_info, &query->sign_txn.initiate, sizeof(hedera_sign_txn_initiate_request_t)); + + send_response(HEDERA_SIGN_TXN_RESPONSE_CONFIRMATION_TAG); + delay_scr_init(ui_text_processing, DELAY_SHORT); + return true; +} + +static bool fetch_valid_input(hedera_query_t *query) { + if (!hedera_get_query(query, HEDERA_QUERY_SIGN_TXN_TAG) || + !check_which_request(query, HEDERA_SIGN_TXN_REQUEST_TXN_DATA_TAG)) { + return false; + } + + const hedera_sign_txn_data_t *txn_data = &query->sign_txn.txn_data; + if (!txn_data->has_txn_bytes || txn_data->txn_bytes.size > MAX_TXN_SIZE) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_INVALID_DATA); + return false; + } + + // Store raw transaction bytes for signing + hedera_txn_context->raw_txn_len = txn_data->txn_bytes.size; + memcpy(hedera_txn_context->raw_txn_bytes, txn_data->txn_bytes.bytes, hedera_txn_context->raw_txn_len); + + // Decode protobuf message for UI display + pb_istream_t stream = pb_istream_from_buffer(txn_data->txn_bytes.bytes, txn_data->txn_bytes.size); + if (!pb_decode(&stream, Hedera_TransactionBody_fields, &hedera_txn_context->txn)) { + hedera_send_error(ERROR_COMMON_ERROR_CORRUPT_DATA_TAG, ERROR_DATA_FLOW_DECODING_FAILED); + return false; + } + + send_response(HEDERA_SIGN_TXN_RESPONSE_UNSIGNED_TXN_ACCEPTED_TAG); + return true; +} + +static bool get_user_verification() { + const Hedera_TransactionBody *txn = &hedera_txn_context->txn; + char display_str[128]; + char temp_str[64]; + + // Verify Operator Account + hedera_format_account_id(&txn->transactionID.accountID, temp_str); + snprintf(display_str, sizeof(display_str), "Operator: %s", temp_str); + if (!core_scroll_page("Verify Transaction", display_str, hedera_send_error)) return false; + + // Verify Transaction Details based on type + switch (txn->which_data) { + case Hedera_TransactionBody_cryptoTransfer_tag: { + const Hedera_CryptoTransferTransactionBody *transfer = &txn->data.cryptoTransfer; + for (int i = 0; i < transfer->transfers.accountAmounts_count; i++) { + const Hedera_AccountAmount *aa = &transfer->transfers.accountAmounts[i]; + if (aa->amount == 0) continue; // Skip operator fee record + + hedera_format_account_id(&aa->accountID, temp_str); + const char *action = (aa->amount > 0) ? "Recipient" : "Sender"; + snprintf(display_str, sizeof(display_str), "%s: %s", action, temp_str); + if (!core_scroll_page("Verify Transfer", display_str, hedera_send_error)) return false; + + hedera_format_tinybars_to_hbar_string(aa->amount, temp_str); + snprintf(display_str, sizeof(display_str), "Amount: %s", temp_str); + if (!core_confirmation(display_str, hedera_send_error)) return false; + } + break; + } + // Add cases for other transaction types (TokenMint, TokenBurn, etc.) here + default: + if (!core_confirmation("Verify transaction details", hedera_send_error)) return false; + break; + } + + // Verify Fee + hedera_format_tinybars_to_hbar_string(txn->transactionFee, temp_str); + snprintf(display_str, sizeof(display_str), "Fee: %s", temp_str); + if (!core_confirmation(display_str, hedera_send_error)) return false; + + // Verify Memo (if present) + if (strlen(txn->memo) > 0) { + if (!core_scroll_page("Memo", txn->memo, hedera_send_error)) return false; + } + + set_app_flow_status(HEDERA_SIGN_TXN_STATUS_VERIFY); + return true; +} + +static bool sign_txn(hedera_sign_txn_signature_response_signature_t *signature) { + uint8_t seed[64] = {0}; + if (!reconstruct_seed(hedera_txn_context->init_info.wallet_id, seed, hedera_send_error)) { + memzero(seed, sizeof(seed)); + return false; + } + + set_app_flow_status(HEDERA_SIGN_TXN_STATUS_SEED_GENERATED); + + HDNode hdnode = {0}; + if (!derive_hdnode_from_path( + hedera_txn_context->init_info.derivation_path, + hedera_txn_context->init_info.derivation_path_count, + ED25519_NAME, + seed, + &hdnode)) { + memzero(seed, sizeof(seed)); + return false; + } + memzero(seed, sizeof(seed)); + + // Ed25519 signs the raw message, not a hash of it. + ed25519_sign(hdnode.private_key, + hedera_txn_context->raw_txn_bytes, + hedera_txn_context->raw_txn_len, + signature->bytes); + signature->size = HEDERA_SIGNATURE_SIZE; + + memzero(&hdnode, sizeof(hdnode)); + return true; +} + +static bool send_signature(hedera_query_t *query, const hedera_sign_txn_signature_response_signature_t *signature) { + hedera_result_t result = init_hedera_result(HEDERA_RESULT_SIGN_TXN_TAG); + result.sign_txn.which_response = HEDERA_SIGN_TXN_RESPONSE_SIGNATURE_TAG; + + if (!hedera_get_query(query, HEDERA_QUERY_SIGN_TXN_TAG) || + !check_which_request(query, HEDERA_SIGN_TXN_REQUEST_SIGNATURE_TAG)) { + return false; + } + + memcpy(&result.sign_txn.signature.signature, signature, sizeof(hedera_sign_txn_signature_response_signature_t)); + hedera_send_result(&result); + return true; +} + +void hedera_sign_transaction(hedera_query_t *query) { + hedera_txn_context = (hedera_txn_context_t *)malloc(sizeof(hedera_txn_context_t)); + memzero(hedera_txn_context, sizeof(hedera_txn_context_t)); + hedera_sign_txn_signature_response_signature_t signature = {0}; + + if (handle_initiate_query(query) && + fetch_valid_input(query) && + get_user_verification() && + sign_txn(&signature) && + send_signature(query, &signature)) { + delay_scr_init(ui_text_check_software_wallet_app, DELAY_TIME); + } + + if (hedera_txn_context) { + free(hedera_txn_context); + hedera_txn_context = NULL; + } +} \ No newline at end of file diff --git a/apps/hedera_app/index.md b/apps/hedera_app/index.md new file mode 100644 index 000000000..0c7222571 --- /dev/null +++ b/apps/hedera_app/index.md @@ -0,0 +1,57 @@ +### Technical Breakdown of the `hedera_app` Implementation + +This application enables the Cypherock X1 wallet to securely manage a user's Hedera (HBAR) account by performing two primary cryptographic functions: exporting a public key and signing a transaction. + +#### 1. Core Architecture & App Registration +* **Entry Point (`hedera_main.c`)**: + * Defines the application descriptor `hedera_app_desc` which includes a unique App ID (e.g., 24) and the version. This descriptor is registered with the main firmware controller in `src/controller_main.c`. + * Implements `hedera_main()`, which acts as the central router for all incoming requests for the Hedera app. It decodes the top-level `hedera_query_t` message to determine the requested action (e.g., `get_public_keys`, `sign_txn`) and calls the appropriate handler. + +* **Communication Layer (`hedera_api.c`, `hedera_api.h`)**: + * Provides a standardized interface for handling Protobuf-based communication with the host. + * **Encoding/Decoding**: Wraps `nanopb`'s `pb_encode` and `pb_decode` functions. `decode_hedera_query` safely parses incoming APDU data into C structs, while `hedera_send_result` serializes C structs back into a byte stream to send to the host. + * **Error Handling**: Implements a consistent error-reporting mechanism (`hedera_send_error`) that sends a `common_error_t` message back to the host, ensuring the host knows precisely why an operation failed. + +#### 2. Cryptographic Operations & Key Management +* **Key Derivation (`hedera_pub_key.c`, `hedera_txn.c`)**: + * **Algorithm**: Utilizes the **Ed25519** signature scheme, which is Hedera's standard. This is specified in calls to `derive_hdnode_from_path`. + * **Derivation Path**: Strictly enforces the BIP-44 path `m/44'/3030'/0'/0'/i'` in `hedera_helpers.c` via `hedera_derivation_path_guard()`. This prevents the wallet from signing with keys from non-standard paths, which is a critical security measure. The `3030'` component is Hedera's registered coin type. + * **Seed Management**: The app is stateless regarding secrets. It never stores the seed. For every cryptographic operation, it calls the core firmware function `reconstruct_seed()` to temporarily reconstruct the master seed in RAM, uses it, and then immediately erases it by calling `memzero()`. + +#### 3. Public Key Export (`hedera_pub_key.c`) +* **Function**: Implements the `hedera_get_pub_keys` flow. +* **Process**: + 1. Receives a wallet ID and one or more derivation paths from the host. + 2. Prompts the user for consent to export a public key for the specified wallet. + 3. Calls `reconstruct_seed()` to get the master seed. + 4. For each path, it calls `derive_hdnode_from_path()` to derive the corresponding Ed25519 key pair. + 5. The **raw 32-byte public key** is extracted. + 6. **User Verification (WYSIWYS)**: If a "user-verified" export is requested, the raw 32-byte public key is converted into a **64-character hexadecimal string** (`hedera_format_pubkey`) and displayed on the device screen for the user to verify. This is crucial because Hedera account IDs (`0.0.123`) are not cryptographically derived from the key. + 7. The raw 32-byte public key(s) are sent back to the host. + +#### 4. Transaction Signing (`hedera_txn.c`) +* **Function**: Implements the `hedera_sign_transaction` flow, the app's most critical feature. +* **Process**: + 1. **Initiation**: Receives a wallet ID and a derivation path. It prompts the user to confirm they want to start a signing process for that wallet. + 2. **Data Reception**: Receives the **raw, serialized Protobuf `TransactionBody`** from the host. This byte array is stored in `hedera_txn_context->raw_txn_bytes`. + 3. **Parsing for Display**: It uses `pb_decode` and the `nanopb`-generated `Hedera_TransactionBody_fields` descriptor to parse `raw_txn_bytes` into the `hedera_txn_context->txn` C struct. This decoding is **only for display and verification purposes**. + 4. **User Verification (WYSIWYS)**: This is the core security step. + * It iterates through the decoded `txn` struct. + * It displays critical information on the screen for user approval, one piece at a time: + * **Operator Account ID**: The account paying the fee (`txn.transactionID.accountID`). + * **Transaction Type**: Determined by `txn.which_data`. The code specifically handles `cryptoTransfer`. + * **Transfers**: For a crypto transfer, it loops through the `accountAmounts` list, displaying each recipient's account ID and the amount. + * **Amount Formatting**: Calls `hedera_format_tinybars_to_hbar_string` to convert the integer `amount` (in tinybars) into a human-readable HBAR string (e.g., "12.345 HBAR"). + * **Transaction Fee**: Formats and displays `txn.transactionFee`. + * **Memo**: Displays the `txn.memo` if it exists. + 5. **Cryptographic Signing**: + * Upon user approval, it calls `reconstruct_seed()` and `derive_hdnode_from_path()` to get the required Ed25519 private key. + * It calls `ed25519_sign()` from the device's crypto library. + * **Crucially, it signs the original `raw_txn_bytes` received in step 2.** It does NOT sign a hash. + * The result is a **64-byte raw signature**. + 6. **Response**: The raw 64-byte signature is sent back to the host. + +#### 5. Helper Utilities (`hedera_helpers.c`, `hedera_context.h`) +* **Context**: Defines all Hedera-specific constants (`HEDERA_NAME`, coin type, key sizes) in one place for easy maintenance. +* **Formatting**: Provides functions to convert raw data types (64-bit integers for amounts, Protobuf structs for account IDs, byte arrays for keys) into user-friendly strings for the UI. This isolates display logic from cryptographic logic. +* **Validation**: Includes the `hedera_derivation_path_guard` to ensure that the app only ever operates on valid, standard derivation paths, preventing many types of attacks.