/* * This file is part of the Trezor project, https://trezor.io/ * * Copyright (C) 2018 ZuluCrypto <zulucrypto@protonmail.com> * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see <http://www.gnu.org/licenses/>. */ // Stellar signing workflow: // // 1. Client sends a StellarSignTx method to the device with transaction header // information // 2. Device confirms transaction details with the user and requests first // operation // 3. Client sends protobuf message with details about the operation to sign // 4. Device confirms operation with user // 5a. If there are more operations in the transaction, device responds with // StellarTxOpRequest. Go to 3 5b. If the operation is the last one, device // responds with StellarSignedTx #include "stellar.h" #include <stdbool.h> #include <time.h> #include "base32.h" #include "bignum.h" #include "bip32.h" #include "config.h" #include "crypto.h" #include "fonts.h" #include "fsm.h" #include "gettext.h" #include "layout2.h" #include "memzero.h" #include "messages.h" #include "messages.pb.h" #include "oled.h" #include "protect.h" #include "util.h" static bool stellar_signing = false; static StellarTransaction stellar_activeTx; /* * Starts the signing process and parses the transaction header */ bool stellar_signingInit(const StellarSignTx *msg) { memzero(&stellar_activeTx, sizeof(StellarTransaction)); stellar_signing = true; // Initialize signing context sha256_Init(&(stellar_activeTx.sha256_ctx)); // Calculate sha256 for network passphrase // max length defined in messages.options uint8_t network_hash[32] = {0}; sha256_Raw((uint8_t *)msg->network_passphrase, strnlen(msg->network_passphrase, 1024), network_hash); uint8_t tx_type_bytes[4] = {0x00, 0x00, 0x00, 0x02}; // Copy some data into the active tx stellar_activeTx.num_operations = msg->num_operations; // Start building what will be signed: // sha256 of: // sha256(network passphrase) // 4-byte unsigned big-endian int type constant (2 for tx) // remaining bytes are operations added in subsequent messages stellar_hashupdate_bytes(network_hash, sizeof(network_hash)); stellar_hashupdate_bytes(tx_type_bytes, sizeof(tx_type_bytes)); // Public key comes from deriving the specified account path const HDNode *node = stellar_deriveNode(msg->address_n, msg->address_n_count); if (!node) { return false; } memcpy(&(stellar_activeTx.signing_pubkey), node->public_key + 1, sizeof(stellar_activeTx.signing_pubkey)); stellar_activeTx.address_n_count = msg->address_n_count; // todo: fix sizeof check memcpy(&(stellar_activeTx.address_n), &(msg->address_n), sizeof(stellar_activeTx.address_n)); // Hash: public key stellar_hashupdate_address(node->public_key + 1); // Hash: fee stellar_hashupdate_uint32(msg->fee); // Hash: sequence number stellar_hashupdate_uint64(msg->sequence_number); // Timebounds are only present if timebounds_start or timebounds_end is // non-zero uint8_t has_timebounds = (msg->timebounds_start > 0 || msg->timebounds_end > 0); if (has_timebounds) { // Hash: the "has timebounds?" boolean stellar_hashupdate_bool(true); // Timebounds are sent as uint32s since that's all we can display, but they // must be hashed as 64-bit values stellar_hashupdate_uint32(0); stellar_hashupdate_uint32(msg->timebounds_start); stellar_hashupdate_uint32(0); stellar_hashupdate_uint32(msg->timebounds_end); } // No timebounds, hash a false boolean else { stellar_hashupdate_bool(false); } // Hash: memo stellar_hashupdate_uint32(msg->memo_type); switch (msg->memo_type) { // None, nothing else to do case 0: break; // Text: 4 bytes (size) + up to 28 bytes case 1: stellar_hashupdate_string((unsigned char *)&(msg->memo_text), strnlen(msg->memo_text, 28)); break; // ID (8 bytes, uint64) case 2: stellar_hashupdate_uint64(msg->memo_id); break; // Hash and return are the same data structure (32 byte tx hash) case 3: case 4: stellar_hashupdate_bytes(msg->memo_hash.bytes, msg->memo_hash.size); break; default: break; } // Hash: number of operations stellar_hashupdate_uint32(msg->num_operations); // Determine what type of network this transaction is for if (strncmp("Public Global Stellar Network ; September 2015", msg->network_passphrase, 1024) == 0) { stellar_activeTx.network_type = 1; } else if (strncmp("Test SDF Network ; September 2015", msg->network_passphrase, 1024) == 0) { stellar_activeTx.network_type = 2; } else { stellar_activeTx.network_type = 3; } return true; } bool stellar_confirmSourceAccount(bool has_source_account, const char *str_account) { if (!has_source_account) { stellar_hashupdate_bool(false); return true; } // Convert account string to public key bytes uint8_t bytes[32] = {0}; if (!stellar_getAddressBytes(str_account, bytes)) { return false; } const char **str_addr_rows = stellar_lineBreakAddress(bytes); stellar_layoutTransactionDialog(_("Op src account OK?"), NULL, str_addr_rows[0], str_addr_rows[1], str_addr_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash: source account stellar_hashupdate_address(bytes); return true; } bool stellar_confirmCreateAccountOp(const StellarCreateAccountOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(0); // Validate new account and convert to bytes uint8_t new_account_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->new_account, new_account_bytes)) { stellar_signingAbort(_("Invalid new account address")); return false; } const char **str_addr_rows = stellar_lineBreakAddress(new_account_bytes); // Amount being funded char str_amount_line[32] = {0}; char str_amount[32] = {0}; stellar_format_stroops(msg->starting_balance, str_amount, sizeof(str_amount)); strlcpy(str_amount_line, _("With "), sizeof(str_amount_line)); strlcat(str_amount_line, str_amount, sizeof(str_amount_line)); strlcat(str_amount_line, _(" XLM"), sizeof(str_amount_line)); stellar_layoutTransactionDialog(_("Create account: "), str_addr_rows[0], str_addr_rows[1], str_addr_rows[2], str_amount_line); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash: address stellar_hashupdate_address(new_account_bytes); // Hash: starting amount stellar_hashupdate_uint64(msg->starting_balance); stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmPaymentOp(const StellarPaymentOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(1); // Validate destination account and convert to bytes uint8_t destination_account_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->destination_account, destination_account_bytes)) { stellar_signingAbort(_("Invalid destination account")); return false; } const char **str_addr_rows = stellar_lineBreakAddress(destination_account_bytes); // To: G... char str_to[32] = {0}; strlcpy(str_to, _("To: "), sizeof(str_to)); strlcat(str_to, str_addr_rows[0], sizeof(str_to)); char str_asset_row[32] = {0}; memzero(str_asset_row, sizeof(str_asset_row)); stellar_format_asset(&(msg->asset), str_asset_row, sizeof(str_asset_row)); char str_pay_amount[32] = {0}; char str_amount[32] = {0}; stellar_format_stroops(msg->amount, str_amount, sizeof(str_amount)); strlcpy(str_pay_amount, _("Pay "), sizeof(str_pay_amount)); strlcat(str_pay_amount, str_amount, sizeof(str_pay_amount)); stellar_layoutTransactionDialog(str_pay_amount, str_asset_row, str_to, str_addr_rows[1], str_addr_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash destination stellar_hashupdate_address(destination_account_bytes); // asset stellar_hashupdate_asset(&(msg->asset)); // amount (even though amount is signed it doesn't matter for hashing) stellar_hashupdate_uint64(msg->amount); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmPathPaymentOp(const StellarPathPaymentOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(2); // Validate destination account and convert to bytes uint8_t destination_account_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->destination_account, destination_account_bytes)) { stellar_signingAbort(_("Invalid destination account")); return false; } const char **str_dest_rows = stellar_lineBreakAddress(destination_account_bytes); // To: G... char str_to[32] = {0}; strlcpy(str_to, _("To: "), sizeof(str_to)); strlcat(str_to, str_dest_rows[0], sizeof(str_to)); char str_send_asset[32] = {0}; char str_dest_asset[32] = {0}; stellar_format_asset(&(msg->send_asset), str_send_asset, sizeof(str_send_asset)); stellar_format_asset(&(msg->destination_asset), str_dest_asset, sizeof(str_dest_asset)); char str_pay_amount[32] = {0}; char str_amount[32] = {0}; stellar_format_stroops(msg->destination_amount, str_amount, sizeof(str_amount)); strlcpy(str_pay_amount, _("Path Pay "), sizeof(str_pay_amount)); strlcat(str_pay_amount, str_amount, sizeof(str_pay_amount)); // Confirm what the receiver will get /* Path Pay 100 JPY (G1234ABCDEF) To: G.... .... .... */ stellar_layoutTransactionDialog(str_pay_amount, str_dest_asset, str_to, str_dest_rows[1], str_dest_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Confirm what the sender is using to pay char str_source_amount[32] = {0}; char str_source_number[32] = {0}; stellar_format_stroops(msg->send_max, str_source_number, sizeof(str_source_number)); strlcpy(str_source_amount, _("Pay Using "), sizeof(str_source_amount)); strlcat(str_source_amount, str_source_number, sizeof(str_source_amount)); stellar_layoutTransactionDialog(str_source_amount, str_send_asset, NULL, _("This is the amount debited"), _("from your account.")); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Note: no confirmation for intermediate steps since they don't impact the // user // Hash send asset stellar_hashupdate_asset(&(msg->send_asset)); // send max (signed vs. unsigned doesn't matter wrt hashing) stellar_hashupdate_uint64(msg->send_max); // destination account stellar_hashupdate_address(destination_account_bytes); // destination asset stellar_hashupdate_asset(&(msg->destination_asset)); // destination amount stellar_hashupdate_uint64(msg->destination_amount); // paths are stored as an array so hash the number of elements as a uint32 stellar_hashupdate_uint32(msg->paths_count); for (uint8_t i = 0; i < msg->paths_count; i++) { stellar_hashupdate_asset(&(msg->paths[i])); } // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmManageOfferOp(const StellarManageOfferOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(3); // New Offer / Delete #123 / Update #123 char str_offer[32] = {0}; if (msg->offer_id == 0) { strlcpy(str_offer, _("New Offer"), sizeof(str_offer)); } else { char str_offer_id[20] = {0}; stellar_format_uint64(msg->offer_id, str_offer_id, sizeof(str_offer_id)); if (msg->amount == 0) { strlcpy(str_offer, _("Delete #"), sizeof(str_offer)); } else { strlcpy(str_offer, _("Update #"), sizeof(str_offer)); } strlcat(str_offer, str_offer_id, sizeof(str_offer)); } char str_selling[32] = {0}; char str_sell_amount[32] = {0}; char str_selling_asset[32] = {0}; stellar_format_asset(&(msg->selling_asset), str_selling_asset, sizeof(str_selling_asset)); stellar_format_stroops(msg->amount, str_sell_amount, sizeof(str_sell_amount)); /* Sell 200 XLM (Native Asset) */ strlcpy(str_selling, _("Sell "), sizeof(str_selling)); strlcat(str_selling, str_sell_amount, sizeof(str_selling)); char str_buying[32] = {0}; char str_buying_asset[32] = {0}; char str_price[32] = {0}; stellar_format_asset(&(msg->buying_asset), str_buying_asset, sizeof(str_buying_asset)); stellar_format_price(msg->price_n, msg->price_d, str_price, sizeof(str_price)); /* For 0.675952 Per USD (G12345678) */ strlcpy(str_buying, _("For "), sizeof(str_buying)); strlcat(str_buying, str_price, sizeof(str_buying)); strlcat(str_buying, _(" Per"), sizeof(str_buying)); stellar_layoutTransactionDialog(str_offer, str_selling, str_selling_asset, str_buying, str_buying_asset); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash selling asset stellar_hashupdate_asset(&(msg->selling_asset)); // buying asset stellar_hashupdate_asset(&(msg->buying_asset)); // amount to sell (signed vs. unsigned doesn't matter wrt hashing) stellar_hashupdate_uint64(msg->amount); // numerator stellar_hashupdate_uint32(msg->price_n); // denominator stellar_hashupdate_uint32(msg->price_d); // offer ID stellar_hashupdate_uint64(msg->offer_id); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmCreatePassiveOfferOp( const StellarCreatePassiveOfferOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(4); // New Offer / Delete #123 / Update #123 char str_offer[32] = {0}; if (msg->amount == 0) { strlcpy(str_offer, _("Delete Passive Offer"), sizeof(str_offer)); } else { strlcpy(str_offer, _("New Passive Offer"), sizeof(str_offer)); } char str_selling[32] = {0}; char str_sell_amount[32] = {0}; char str_selling_asset[32] = {0}; stellar_format_asset(&(msg->selling_asset), str_selling_asset, sizeof(str_selling_asset)); stellar_format_stroops(msg->amount, str_sell_amount, sizeof(str_sell_amount)); /* Sell 200 XLM (Native Asset) */ strlcpy(str_selling, _("Sell "), sizeof(str_selling)); strlcat(str_selling, str_sell_amount, sizeof(str_selling)); char str_buying[32] = {0}; char str_buying_asset[32] = {0}; char str_price[32] = {0}; stellar_format_asset(&(msg->buying_asset), str_buying_asset, sizeof(str_buying_asset)); stellar_format_price(msg->price_n, msg->price_d, str_price, sizeof(str_price)); /* For 0.675952 Per USD (G12345678) */ strlcpy(str_buying, _("For "), sizeof(str_buying)); strlcat(str_buying, str_price, sizeof(str_buying)); strlcat(str_buying, _(" Per"), sizeof(str_buying)); stellar_layoutTransactionDialog(str_offer, str_selling, str_selling_asset, str_buying, str_buying_asset); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash selling asset stellar_hashupdate_asset(&(msg->selling_asset)); // buying asset stellar_hashupdate_asset(&(msg->buying_asset)); // amount to sell (signed vs. unsigned doesn't matter wrt hashing) stellar_hashupdate_uint64(msg->amount); // numerator stellar_hashupdate_uint32(msg->price_n); // denominator stellar_hashupdate_uint32(msg->price_d); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmSetOptionsOp(const StellarSetOptionsOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(5); // Something like Set Inflation Destination char str_title[32] = {0}; char rows[4][32] = {0}; int row_idx = 0; memzero(rows, sizeof(rows)); // Inflation destination stellar_hashupdate_bool(msg->has_inflation_destination_account); if (msg->has_inflation_destination_account) { strlcpy(str_title, _("Set Inflation Destination"), sizeof(str_title)); // Validate account and convert to bytes uint8_t inflation_destination_account_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->inflation_destination_account, inflation_destination_account_bytes)) { stellar_signingAbort(_("Invalid inflation destination account")); return false; } const char **str_addr_rows = stellar_lineBreakAddress(inflation_destination_account_bytes); stellar_layoutTransactionDialog(str_title, NULL, str_addr_rows[0], str_addr_rows[1], str_addr_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // address stellar_hashupdate_address(inflation_destination_account_bytes); } // Clear flags stellar_hashupdate_bool(msg->has_clear_flags); if (msg->has_clear_flags) { strlcpy(str_title, _("Clear Flag(s)"), sizeof(str_title)); // Auth required if (msg->clear_flags > 7) { stellar_signingAbort(_("Invalid flags")); return false; } if (msg->clear_flags & 0x01) { strlcpy(rows[row_idx], _("AUTH_REQUIRED"), sizeof(rows[row_idx])); row_idx++; } // Auth revocable if (msg->clear_flags & 0x02) { strlcpy(rows[row_idx], _("AUTH_REVOCABLE"), sizeof(rows[row_idx])); row_idx++; } // Auth immutable if (msg->clear_flags & 0x04) { strlcpy(rows[row_idx], _("AUTH_IMMUTABLE"), sizeof(rows[row_idx])); row_idx++; } stellar_layoutTransactionDialog(str_title, rows[0], rows[1], rows[2], rows[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } memzero(rows, sizeof(rows)); row_idx = 0; // Hash flags stellar_hashupdate_uint32(msg->clear_flags); } // Set flags stellar_hashupdate_bool(msg->has_set_flags); if (msg->has_set_flags) { strlcpy(str_title, _("Set Flag(s)"), sizeof(str_title)); // Auth required if (msg->set_flags > 7) { stellar_signingAbort(_("Invalid flags")); return false; } if (msg->set_flags & 0x01) { strlcpy(rows[row_idx], _("AUTH_REQUIRED"), sizeof(rows[row_idx])); row_idx++; } // Auth revocable if (msg->set_flags & 0x02) { strlcpy(rows[row_idx], _("AUTH_REVOCABLE"), sizeof(rows[row_idx])); row_idx++; } // Auth immutable if (msg->set_flags & 0x04) { strlcpy(rows[row_idx], _("AUTH_IMMUTABLE"), sizeof(rows[row_idx])); row_idx++; } stellar_layoutTransactionDialog(str_title, rows[0], rows[1], rows[2], rows[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } memzero(rows, sizeof(rows)); row_idx = 0; // Hash flags stellar_hashupdate_uint32(msg->set_flags); } // Account thresholds bool show_thresholds_confirm = false; row_idx = 0; stellar_hashupdate_bool(msg->has_master_weight); if (msg->has_master_weight) { char str_master_weight[10 + 1] = {0}; show_thresholds_confirm = true; stellar_format_uint32(msg->master_weight, str_master_weight, sizeof(str_master_weight)); strlcpy(rows[row_idx], _("Master Weight: "), sizeof(rows[row_idx])); strlcat(rows[row_idx], str_master_weight, sizeof(rows[row_idx])); row_idx++; // Hash master weight stellar_hashupdate_uint32(msg->master_weight); } stellar_hashupdate_bool(msg->has_low_threshold); if (msg->has_low_threshold) { char str_low_threshold[10 + 1] = {0}; show_thresholds_confirm = true; stellar_format_uint32(msg->low_threshold, str_low_threshold, sizeof(str_low_threshold)); strlcpy(rows[row_idx], _("Low: "), sizeof(rows[row_idx])); strlcat(rows[row_idx], str_low_threshold, sizeof(rows[row_idx])); row_idx++; // Hash low threshold stellar_hashupdate_uint32(msg->low_threshold); } stellar_hashupdate_bool(msg->has_medium_threshold); if (msg->has_medium_threshold) { char str_med_threshold[10 + 1] = {0}; show_thresholds_confirm = true; stellar_format_uint32(msg->medium_threshold, str_med_threshold, sizeof(str_med_threshold)); strlcpy(rows[row_idx], _("Medium: "), sizeof(rows[row_idx])); strlcat(rows[row_idx], str_med_threshold, sizeof(rows[row_idx])); row_idx++; // Hash medium threshold stellar_hashupdate_uint32(msg->medium_threshold); } stellar_hashupdate_bool(msg->has_high_threshold); if (msg->has_high_threshold) { char str_high_threshold[10 + 1] = {0}; show_thresholds_confirm = true; stellar_format_uint32(msg->high_threshold, str_high_threshold, sizeof(str_high_threshold)); strlcpy(rows[row_idx], _("High: "), sizeof(rows[row_idx])); strlcat(rows[row_idx], str_high_threshold, sizeof(rows[row_idx])); row_idx++; // Hash high threshold stellar_hashupdate_uint32(msg->high_threshold); } if (show_thresholds_confirm) { strlcpy(str_title, _("Account Thresholds"), sizeof(str_title)); stellar_layoutTransactionDialog(str_title, rows[0], rows[1], rows[2], rows[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } memzero(rows, sizeof(rows)); row_idx = 0; } // Home domain stellar_hashupdate_bool(msg->has_home_domain); if (msg->has_home_domain) { strlcpy(str_title, _("Home Domain"), sizeof(str_title)); // Split home domain if longer than 22 characters int home_domain_len = strnlen(msg->home_domain, 32); if (home_domain_len > 22) { strlcpy(rows[0], msg->home_domain, 22); strlcpy(rows[1], msg->home_domain + 21, sizeof(rows[1])); } else { strlcpy(rows[0], msg->home_domain, sizeof(rows[0])); } stellar_layoutTransactionDialog(str_title, rows[0], rows[1], NULL, NULL); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } memzero(rows, sizeof(rows)); row_idx = 0; stellar_hashupdate_string((unsigned char *)&(msg->home_domain), strnlen(msg->home_domain, 32)); } // Signer stellar_hashupdate_bool(msg->has_signer_type); if (msg->has_signer_type) { if (msg->signer_weight > 0) { strlcpy(str_title, _("Add Signer: "), sizeof(str_title)); } else { strlcpy(str_title, _("REMOVE Signer: "), sizeof(str_title)); } // Format weight as a string char str_weight[16] = {0}; stellar_format_uint32(msg->signer_weight, str_weight, sizeof(str_weight)); char str_weight_row[32] = {0}; strlcpy(str_weight_row, _("Weight: "), sizeof(str_weight_row)); strlcat(str_weight_row, str_weight, sizeof(str_weight_row)); // 0 = account, 1 = pre-auth, 2 = hash(x) char str_signer_type[16] = {0}; bool needs_hash_confirm = false; if (msg->signer_type == 0) { strlcpy(str_signer_type, _("account"), sizeof(str_signer_type)); strlcat(str_title, str_signer_type, sizeof(str_title)); const char **str_addr_rows = stellar_lineBreakAddress(msg->signer_key.bytes); stellar_layoutTransactionDialog(str_title, str_weight_row, str_addr_rows[0], str_addr_rows[1], str_addr_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } } if (msg->signer_type == 1) { needs_hash_confirm = true; strlcpy(str_signer_type, _("pre-auth hash"), sizeof(str_signer_type)); strlcat(str_title, str_signer_type, sizeof(str_title)); stellar_layoutTransactionDialog(str_title, str_weight_row, NULL, _("(confirm hash on next"), _("screen)")); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } } if (msg->signer_type == 2) { needs_hash_confirm = true; strlcpy(str_signer_type, _("hash(x)"), sizeof(str_signer_type)); strlcat(str_title, str_signer_type, sizeof(str_title)); stellar_layoutTransactionDialog(str_title, str_weight_row, NULL, _("(confirm hash on next"), _("screen)")); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } } // Extra confirmation step for hash signers if (needs_hash_confirm) { data2hex(msg->signer_key.bytes + 0, 8, rows[row_idx++]); data2hex(msg->signer_key.bytes + 8, 8, rows[row_idx++]); data2hex(msg->signer_key.bytes + 16, 8, rows[row_idx++]); data2hex(msg->signer_key.bytes + 24, 8, rows[row_idx++]); stellar_layoutTransactionDialog(_("Confirm Hash"), rows[0], rows[1], rows[2], rows[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } memzero(rows, sizeof(rows)); row_idx = 0; } // Hash: signer type stellar_hashupdate_uint32(msg->signer_type); // key stellar_hashupdate_bytes(msg->signer_key.bytes, msg->signer_key.size); // weight stellar_hashupdate_uint32(msg->signer_weight); } // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmChangeTrustOp(const StellarChangeTrustOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(6); // Add Trust: USD char str_title[32] = {0}; if (msg->limit == 0) { strlcpy(str_title, _("DELETE Trust: "), sizeof(str_title)); } else { strlcpy(str_title, _("Add Trust: "), sizeof(str_title)); } strlcat(str_title, msg->asset.code, sizeof(str_title)); // Amount: MAX (or a number) char str_amount_row[32] = {0}; strlcpy(str_amount_row, _("Amount: "), sizeof(str_amount_row)); if (msg->limit == 9223372036854775807) { strlcat(str_amount_row, _("[Maximum]"), sizeof(str_amount_row)); } else { char str_amount[32] = {0}; stellar_format_stroops(msg->limit, str_amount, sizeof(str_amount)); strlcat(str_amount_row, str_amount, sizeof(str_amount_row)); } // Validate destination account and convert to bytes uint8_t asset_issuer_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->asset.issuer, asset_issuer_bytes)) { stellar_signingAbort(_("User canceled")); fsm_sendFailure(FailureType_Failure_ProcessError, _("Invalid asset issuer")); return false; } // Display full issuer address const char **str_addr_rows = stellar_lineBreakAddress(asset_issuer_bytes); stellar_layoutTransactionDialog(str_title, str_amount_row, str_addr_rows[0], str_addr_rows[1], str_addr_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash: asset stellar_hashupdate_asset(&(msg->asset)); // limit stellar_hashupdate_uint64(msg->limit); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmAllowTrustOp(const StellarAllowTrustOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(7); // Add Trust: USD char str_title[32] = {0}; if (msg->is_authorized) { strlcpy(str_title, _("Allow Trust of"), sizeof(str_title)); } else { strlcpy(str_title, _("REVOKE Trust of"), sizeof(str_title)); } // Asset code char str_asset_row[32] = {0}; strlcpy(str_asset_row, msg->asset_code, sizeof(str_asset_row)); // Validate account and convert to bytes uint8_t trusted_account_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->trusted_account, trusted_account_bytes)) { stellar_signingAbort(_("Invalid trusted account")); return false; } const char **str_trustor_rows = stellar_lineBreakAddress(trusted_account_bytes); // By: G... char str_by[32] = {0}; strlcpy(str_by, _("By: "), sizeof(str_by)); strlcat(str_by, str_trustor_rows[0], sizeof(str_by)); stellar_layoutTransactionDialog(str_title, str_asset_row, str_by, str_trustor_rows[1], str_trustor_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash: trustor account (the account being allowed to access the asset) stellar_hashupdate_address(trusted_account_bytes); // asset type stellar_hashupdate_uint32(msg->asset_type); // asset code if (msg->asset_type == 1) { char code4[4 + 1] = {0}; memzero(code4, sizeof(code4)); strlcpy(code4, msg->asset_code, sizeof(code4)); stellar_hashupdate_bytes((uint8_t *)code4, 4); } if (msg->asset_type == 2) { char code12[12 + 1] = {0}; memzero(code12, sizeof(code12)); strlcpy(code12, msg->asset_code, sizeof(code12)); stellar_hashupdate_bytes((uint8_t *)code12, 12); } // is authorized stellar_hashupdate_bool(msg->is_authorized); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmAccountMergeOp(const StellarAccountMergeOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(8); // Validate account and convert to bytes uint8_t destination_account_bytes[STELLAR_KEY_SIZE] = {0}; if (!stellar_getAddressBytes(msg->destination_account, destination_account_bytes)) { stellar_signingAbort(_("Invalid destination account")); return false; } const char **str_destination_rows = stellar_lineBreakAddress(destination_account_bytes); stellar_layoutTransactionDialog( _("Merge Account"), _("All XLM will be sent to:"), str_destination_rows[0], str_destination_rows[1], str_destination_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash: destination account stellar_hashupdate_address(destination_account_bytes); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmManageDataOp(const StellarManageDataOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(10); char str_title[32] = {0}; if (msg->has_value) { strlcpy(str_title, _("Set data value key:"), sizeof(str_title)); } else { strlcpy(str_title, _("CLEAR data value key:"), sizeof(str_title)); } // Confirm key const char **str_key_lines = split_message((const uint8_t *)(msg->key), strnlen(msg->key, 64), 16); stellar_layoutTransactionDialog(str_title, str_key_lines[0], str_key_lines[1], str_key_lines[2], str_key_lines[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Confirm value by displaying sha256 hash since this can contain // non-printable characters if (msg->has_value) { strlcpy(str_title, _("Confirm sha256 of value:"), sizeof(str_title)); char str_hash_digest[SHA256_DIGEST_STRING_LENGTH] = {0}; sha256_Data(msg->value.bytes, msg->value.size, str_hash_digest); const char **str_hash_lines = split_message( (const uint8_t *)str_hash_digest, sizeof(str_hash_digest), 16); stellar_layoutTransactionDialog(str_title, str_hash_lines[0], str_hash_lines[1], str_hash_lines[2], str_hash_lines[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } } // Hash: key stellar_hashupdate_string((unsigned char *)&(msg->key), strnlen(msg->key, 64)); // value if (msg->has_value) { stellar_hashupdate_bool(true); stellar_hashupdate_string(msg->value.bytes, msg->value.size); } else { stellar_hashupdate_bool(false); } // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } bool stellar_confirmBumpSequenceOp(const StellarBumpSequenceOp *msg) { if (!stellar_signing) return false; if (!stellar_confirmSourceAccount(msg->has_source_account, msg->source_account)) { stellar_signingAbort(_("Source account error")); return false; } // Hash: operation type stellar_hashupdate_uint32(11); char str_bump_to[20] = {0}; stellar_format_uint64(msg->bump_to, str_bump_to, sizeof(str_bump_to)); stellar_layoutTransactionDialog(_("Bump Sequence"), _("Set sequence to:"), str_bump_to, NULL, NULL); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; } // Hash: bump to stellar_hashupdate_uint64(msg->bump_to); // At this point, the operation is confirmed stellar_activeTx.confirmed_operations++; return true; } void stellar_signingAbort(const char *reason) { if (!reason) { reason = _("Unknown error"); } stellar_signing = false; fsm_sendFailure(FailureType_Failure_ProcessError, reason); layoutHome(); } /** * Populates the fields of resp with the signature of the active transaction */ void stellar_fillSignedTx(StellarSignedTx *resp) { // Finalize the transaction by hashing 4 null bytes representing a (currently // unused) empty union stellar_hashupdate_uint32(0); // Add the public key for verification that the right account was used for // signing memcpy(resp->public_key.bytes, stellar_activeTx.signing_pubkey, 32); resp->public_key.size = 32; // Add the signature (note that this does not include the 4-byte hint since it // can be calculated from the public key) uint8_t signature[64] = {0}; // Note: this calls sha256_Final on the hash context stellar_getSignatureForActiveTx(signature); memcpy(resp->signature.bytes, signature, sizeof(signature)); resp->signature.size = sizeof(signature); } bool stellar_allOperationsConfirmed() { return stellar_activeTx.confirmed_operations == stellar_activeTx.num_operations; } /* * Calculates and sets the signature for the active transaction */ void stellar_getSignatureForActiveTx(uint8_t *out_signature) { const HDNode *node = stellar_deriveNode(stellar_activeTx.address_n, stellar_activeTx.address_n_count); if (!node) { // return empty signature when we can't derive node memzero(out_signature, 64); return; } // Signature is the ed25519 detached signature of the sha256 of all the bytes // that have been read so far uint8_t to_sign[32] = {0}; sha256_Final(&(stellar_activeTx.sha256_ctx), to_sign); uint8_t signature[64] = {0}; ed25519_sign(to_sign, sizeof(to_sign), node->private_key, node->public_key + 1, signature); memcpy(out_signature, signature, sizeof(signature)); } /* * Returns number (representing stroops) formatted as XLM * For example, if number has value 1000000000 then it will be returned as * "100.0" */ void stellar_format_stroops(uint64_t number, char *out, size_t outlen) { bn_format_uint64(number, NULL, NULL, 7, 0, false, out, outlen); } /* * Formats a price represented as a uint32 numerator and uint32 denominator * * Note that there may be a loss of precision between the real price value and * what is shown to the user * * Smallest possible price is 1 / 4294967296 which is: * 0.00000000023283064365386962890625 * * largest possible price is: * 4294967296 */ void stellar_format_price(uint32_t numerator, uint32_t denominator, char *out, size_t outlen) { memzero(out, outlen); // early exit for invalid denominator if (denominator == 0) { strlcpy(out, _("[Invalid Price]"), outlen); return; } // early exit for zero if (numerator == 0) { strlcpy(out, "0", outlen); return; } int scale = 0; uint64_t value = numerator; while (value < (UINT64_MAX / 10)) { value *= 10; scale++; } value /= denominator; while (value < (UINT64_MAX / 10)) { value *= 10; scale++; } // Format with bn_format_uint64 bn_format_uint64(value, NULL, NULL, 6, 6 - scale, true, out, outlen); } /* * Returns a uint32 formatted as a string */ void stellar_format_uint32(uint32_t number, char *out, size_t outlen) { bignum256 bn_number = {0}; bn_read_uint32(number, &bn_number); bn_format(&bn_number, NULL, NULL, 0, 0, false, out, outlen); } /* * Returns a uint64 formatted as a string */ void stellar_format_uint64(uint64_t number, char *out, size_t outlen) { bn_format_uint64(number, NULL, NULL, 0, 0, false, out, outlen); } /* * Breaks a 56 character address into 3 lines of lengths 16, 20, 20 * This is to allow a small label to be prepended to the first line */ const char **stellar_lineBreakAddress(const uint8_t *addrbytes) { char str_fulladdr[56 + 1] = {0}; static char rows[3][20 + 1]; memzero(rows, sizeof(rows)); // get full address string stellar_publicAddressAsStr(addrbytes, str_fulladdr, sizeof(str_fulladdr)); // Break it into 3 lines strlcpy(rows[0], str_fulladdr + 0, 17); strlcpy(rows[1], str_fulladdr + 16, 21); strlcpy(rows[2], str_fulladdr + 16 + 20, 21); static const char *ret[3] = {rows[0], rows[1], rows[2]}; return ret; } /* * Returns the asset formatted to fit in a single row * * Examples: * XLM (Native Asset) * MOBI (G123456789000) * ALPHA12EXAMP (G0987) */ void stellar_format_asset(const StellarAssetType *asset, char *str_formatted, size_t len) { char str_asset_code[12 + 1] = {0}; // truncated asset issuer, final length depends on length of asset code char str_asset_issuer_trunc[13 + 1] = {0}; memzero(str_formatted, len); memzero(str_asset_code, sizeof(str_asset_code)); memzero(str_asset_issuer_trunc, sizeof(str_asset_issuer_trunc)); // Validate issuer account for non-native assets if (asset->type != 0 && !stellar_validateAddress(asset->issuer)) { stellar_signingAbort(_("Invalid asset issuer")); return; } // Native asset if (asset->type == 0) { strlcpy(str_formatted, _("XLM (native asset)"), len); } // 4-character custom if (asset->type == 1) { memcpy(str_asset_code, asset->code, 4); strlcpy(str_formatted, str_asset_code, len); // Truncate issuer to 13 chars memcpy(str_asset_issuer_trunc, asset->issuer, 13); } // 12-character custom if (asset->type == 2) { memcpy(str_asset_code, asset->code, 12); strlcpy(str_formatted, str_asset_code, len); // Truncate issuer to 5 characters memcpy(str_asset_issuer_trunc, asset->issuer, 5); } // Issuer is read the same way for both types of custom assets if (asset->type == 1 || asset->type == 2) { strlcat(str_formatted, _(" ("), len); strlcat(str_formatted, str_asset_issuer_trunc, len); strlcat(str_formatted, _(")"), len); } } size_t stellar_publicAddressAsStr(const uint8_t *bytes, char *out, size_t outlen) { // version + key bytes + checksum uint8_t keylen = 1 + 32 + 2; uint8_t bytes_full[keylen]; memset(bytes_full, 0, sizeof(bytes_full)); bytes_full[0] = 6 << 3; // 'G' memcpy(bytes_full + 1, bytes, 32); // Last two bytes are the checksum uint16_t checksum = stellar_crc16(bytes_full, 33); bytes_full[keylen - 2] = checksum & 0x00ff; bytes_full[keylen - 1] = (checksum >> 8) & 0x00ff; base32_encode(bytes_full, keylen, out, outlen, BASE32_ALPHABET_RFC4648); // Public key will always be 56 characters return 56; } /** * Stellar account string is a base32-encoded string that starts with "G" * * It decodes to the following format: * Byte 0 - always 0x30 ("G" when base32 encoded), version byte indicating a * public key Bytes 1-33 - 32-byte public key bytes Bytes 34-35 - 2-byte CRC16 * checksum of the version byte + public key bytes (first 33 bytes) * * Note that the stellar "seed" (private key) also uses this format except the * version byte is 0xC0 which encodes to "S" in base32 */ bool stellar_validateAddress(const char *str_address) { bool valid = false; uint8_t decoded[STELLAR_ADDRESS_SIZE_RAW] = {0}; memzero(decoded, sizeof(decoded)); if (strlen(str_address) != STELLAR_ADDRESS_SIZE) { return false; } // Check that it decodes correctly uint8_t *ret = base32_decode(str_address, STELLAR_ADDRESS_SIZE, decoded, sizeof(decoded), BASE32_ALPHABET_RFC4648); valid = (ret != NULL); // ... and that version byte is 0x30 if (valid && decoded[0] != 0x30) { valid = false; } // ... and that checksums match uint16_t checksum_expected = stellar_crc16(decoded, 33); uint16_t checksum_actual = (decoded[34] << 8) | decoded[33]; // unsigned short (little endian) if (valid && checksum_expected != checksum_actual) { valid = false; } memzero(decoded, sizeof(decoded)); return valid; } /** * Converts a string address (G...) to the 32-byte raw address */ bool stellar_getAddressBytes(const char *str_address, uint8_t *out_bytes) { uint8_t decoded[STELLAR_ADDRESS_SIZE_RAW] = {0}; memzero(decoded, sizeof(decoded)); // Ensure address is valid if (!stellar_validateAddress(str_address)) return false; base32_decode(str_address, STELLAR_ADDRESS_SIZE, decoded, sizeof(decoded), BASE32_ALPHABET_RFC4648); // The 32 bytes with offset 1-33 represent the public key memcpy(out_bytes, &decoded[1], 32); memzero(decoded, sizeof(decoded)); return true; } /* * CRC16 implementation compatible with the Stellar version * Ported from this implementation: * http://introcs.cs.princeton.edu/java/61data/CRC16CCITT.java.html Initial * value changed to 0x0000 to match Stellar */ uint16_t stellar_crc16(uint8_t *bytes, uint32_t length) { // Calculate checksum for existing bytes uint16_t crc = 0x0000; uint16_t polynomial = 0x1021; uint32_t i = 0; uint8_t bit = 0; uint8_t byte = 0; uint8_t bitidx = 0; uint8_t c15 = 0; for (i = 0; i < length; i++) { byte = bytes[i]; for (bitidx = 0; bitidx < 8; bitidx++) { bit = ((byte >> (7 - bitidx) & 1) == 1); c15 = ((crc >> 15 & 1) == 1); crc <<= 1; if (c15 ^ bit) crc ^= polynomial; } } return crc & 0xffff; } /* * Derives the HDNode at the given index * Standard Stellar prefix is m/44'/148'/ and the default account is * m/44'/148'/0' * * All paths must be hardened */ const HDNode *stellar_deriveNode(const uint32_t *address_n, size_t address_n_count) { static CONFIDENTIAL HDNode node; const char *curve = "ed25519"; // Device not initialized, passphrase request cancelled, or unsupported curve if (!config_getRootNode(&node, curve)) { return 0; } // Failed to derive private key if (hdnode_private_ckd_cached(&node, address_n, address_n_count, NULL) == 0) { return 0; } hdnode_fill_public_key(&node); return &node; } void stellar_hashupdate_uint32(uint32_t value) { // Ensure uint32 is big endian #if BYTE_ORDER == LITTLE_ENDIAN REVERSE32(value, value); #endif // Byte values must be hashed as big endian uint8_t data[4] = {0}; data[3] = (value >> 24) & 0xFF; data[2] = (value >> 16) & 0xFF; data[1] = (value >> 8) & 0xFF; data[0] = value & 0xFF; stellar_hashupdate_bytes(data, sizeof(data)); } void stellar_hashupdate_uint64(uint64_t value) { // Ensure uint64 is big endian #if BYTE_ORDER == LITTLE_ENDIAN REVERSE64(value, value); #endif // Byte values must be hashed as big endian uint8_t data[8] = {0}; data[7] = (value >> 56) & 0xFF; data[6] = (value >> 48) & 0xFF; data[5] = (value >> 40) & 0xFF; data[4] = (value >> 32) & 0xFF; data[3] = (value >> 24) & 0xFF; data[2] = (value >> 16) & 0xFF; data[1] = (value >> 8) & 0xFF; data[0] = value & 0xFF; stellar_hashupdate_bytes(data, sizeof(data)); } void stellar_hashupdate_bool(bool value) { if (value) { stellar_hashupdate_uint32(1); } else { stellar_hashupdate_uint32(0); } } void stellar_hashupdate_string(const uint8_t *data, size_t len) { // Hash the length of the string stellar_hashupdate_uint32((uint32_t)len); // Hash the raw bytes of the string stellar_hashupdate_bytes(data, len); // If len isn't a multiple of 4, add padding bytes int remainder = len % 4; uint8_t null_byte[1] = {0x00}; if (remainder) { while (remainder < 4) { stellar_hashupdate_bytes(null_byte, 1); remainder++; } } } void stellar_hashupdate_address(const uint8_t *address_bytes) { // First 4 bytes of an address are the type. There's only one type (0) stellar_hashupdate_uint32(0); // Remaining part of the address is 32 bytes stellar_hashupdate_bytes(address_bytes, 32); } /* * Note about string handling below: this field is an XDR "opaque" field and not * a typical string, so if "TEST" is the asset code then the hashed value needs * to be 4 bytes and not include the null at the end of the string */ void stellar_hashupdate_asset(const StellarAssetType *asset) { stellar_hashupdate_uint32(asset->type); // For non-native assets, validate issuer account and convert to bytes uint8_t issuer_bytes[STELLAR_KEY_SIZE] = {0}; if (asset->type != 0 && !stellar_getAddressBytes(asset->issuer, issuer_bytes)) { stellar_signingAbort(_("Invalid asset issuer")); return; } // 4-character asset code if (asset->type == 1) { char code4[4 + 1] = {0}; memzero(code4, sizeof(code4)); strlcpy(code4, asset->code, sizeof(code4)); stellar_hashupdate_bytes((uint8_t *)code4, 4); stellar_hashupdate_address(issuer_bytes); } // 12-character asset code if (asset->type == 2) { char code12[12 + 1] = {0}; memzero(code12, sizeof(code12)); strlcpy(code12, asset->code, sizeof(code12)); stellar_hashupdate_bytes((uint8_t *)code12, 12); stellar_hashupdate_address(issuer_bytes); } } void stellar_hashupdate_bytes(const uint8_t *data, size_t len) { sha256_Update(&(stellar_activeTx.sha256_ctx), data, len); } /* * Displays a summary of the overall transaction */ void stellar_layoutTransactionSummary(const StellarSignTx *msg) { char str_lines[5][32] = {0}; memzero(str_lines, sizeof(str_lines)); char str_fee[12] = {0}; char str_num_ops[12] = {0}; // Will be set to true for some large hashes that don't fit on one screen uint8_t needs_memo_hash_confirm = 0; // Format the fee stellar_format_stroops(msg->fee, str_fee, sizeof(str_fee)); strlcpy(str_lines[0], _("Fee: "), sizeof(str_lines[0])); strlcat(str_lines[0], str_fee, sizeof(str_lines[0])); strlcat(str_lines[0], _(" XLM"), sizeof(str_lines[0])); // add in numOperations stellar_format_uint32(msg->num_operations, str_num_ops, sizeof(str_num_ops)); strlcat(str_lines[0], _(" ("), sizeof(str_lines[0])); strlcat(str_lines[0], str_num_ops, sizeof(str_lines[0])); if (msg->num_operations == 1) { strlcat(str_lines[0], _(" op)"), sizeof(str_lines[0])); } else { strlcat(str_lines[0], _(" ops)"), sizeof(str_lines[0])); } // Display full address being used to sign transaction const char **str_addr_rows = stellar_lineBreakAddress(stellar_activeTx.signing_pubkey); stellar_layoutTransactionDialog(str_lines[0], _("Signing with:"), str_addr_rows[0], str_addr_rows[1], str_addr_rows[2]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return; } // Reset lines for displaying memo memzero(str_lines, sizeof(str_lines)); // Memo: none if (msg->memo_type == 0) { strlcpy(str_lines[0], _("[No Memo Set]"), sizeof(str_lines[0])); strlcpy(str_lines[1], _("Important:"), sizeof(str_lines[0])); strlcpy(str_lines[2], _("Many exchanges require"), sizeof(str_lines[0])); strlcpy(str_lines[3], _("a memo when depositing."), sizeof(str_lines[0])); } // Memo: text if (msg->memo_type == 1) { strlcpy(str_lines[0], _("Memo (TEXT)"), sizeof(str_lines[0])); // Split 28-character string into two lines of 19 / 9 // todo: word wrap method? strlcpy(str_lines[1], (const char *)msg->memo_text, 19 + 1); strlcpy(str_lines[2], (const char *)(msg->memo_text + 19), 9 + 1); } // Memo: ID if (msg->memo_type == 2) { strlcpy(str_lines[0], _("Memo (ID)"), sizeof(str_lines[0])); stellar_format_uint64(msg->memo_id, str_lines[1], sizeof(str_lines[1])); } // Memo: hash if (msg->memo_type == 3) { needs_memo_hash_confirm = 1; strlcpy(str_lines[0], _("Memo (HASH)"), sizeof(str_lines[0])); } // Memo: return if (msg->memo_type == 4) { needs_memo_hash_confirm = 1; strlcpy(str_lines[0], _("Memo (RETURN)"), sizeof(str_lines[0])); } if (needs_memo_hash_confirm) { data2hex(msg->memo_hash.bytes + 0, 8, str_lines[1]); data2hex(msg->memo_hash.bytes + 8, 8, str_lines[2]); data2hex(msg->memo_hash.bytes + 16, 8, str_lines[3]); data2hex(msg->memo_hash.bytes + 24, 8, str_lines[4]); } stellar_layoutTransactionDialog(str_lines[0], str_lines[1], str_lines[2], str_lines[3], str_lines[4]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return; } // Verify timebounds, if present memzero(str_lines, sizeof(str_lines)); // Timebound: lower if (msg->timebounds_start || msg->timebounds_end) { time_t timebound; char str_timebound[32] = {0}; const struct tm *tm = NULL; timebound = (time_t)msg->timebounds_start; strlcpy(str_lines[0], _("Valid from:"), sizeof(str_lines[0])); if (timebound) { tm = gmtime(&timebound); strftime(str_timebound, sizeof(str_timebound), "%F %T (UTC)", tm); strlcpy(str_lines[1], str_timebound, sizeof(str_lines[1])); } else { strlcpy(str_lines[1], _("[no restriction]"), sizeof(str_lines[1])); } // Reset for timebound_max memzero(str_timebound, sizeof(str_timebound)); timebound = (time_t)msg->timebounds_end; strlcpy(str_lines[2], _("Valid to:"), sizeof(str_lines[2])); if (timebound) { tm = gmtime(&timebound); strftime(str_timebound, sizeof(str_timebound), "%F %T (UTC)", tm); strlcpy(str_lines[3], str_timebound, sizeof(str_lines[3])); } else { strlcpy(str_lines[3], _("[no restriction]"), sizeof(str_lines[3])); } } if (msg->timebounds_start || msg->timebounds_end) { stellar_layoutTransactionDialog(_("Confirm Time Bounds"), str_lines[0], str_lines[1], str_lines[2], str_lines[3]); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return; } } } /* * Most basic dialog used for signing * - Header indicating which key is being used for signing * - 5 rows for content * - Cancel / Next buttons * - Warning message can appear between cancel/next buttons */ void stellar_layoutSigningDialog(const char *line1, const char *line2, const char *line3, const char *line4, const char *line5, uint32_t *address_n, size_t address_n_count, const char *warning, bool is_final_step) { // Start with some initial padding and use these to track position as // rendering moves down the screen int offset_x = 1; int offset_y = 1; int line_height = 9; const HDNode *node = stellar_deriveNode(address_n, address_n_count); if (!node) { // abort on error return; } char str_pubaddr_truncated[12]; // G???? + null memzero(str_pubaddr_truncated, sizeof(str_pubaddr_truncated)); layoutLast = layoutDialogSwipe; layoutSwipe(); oledClear(); // Load up public address char str_pubaddr[56 + 1] = {0}; memzero(str_pubaddr, sizeof(str_pubaddr)); stellar_publicAddressAsStr(node->public_key + 1, str_pubaddr, sizeof(str_pubaddr)); memcpy(str_pubaddr_truncated, str_pubaddr, sizeof(str_pubaddr_truncated) - 1); // Header // Ends up as: Signing with GABCDEFGHIJKL char str_header[32] = {0}; memzero(str_header, sizeof(str_header)); strlcpy(str_header, _("Signing with "), sizeof(str_header)); strlcat(str_header, str_pubaddr_truncated, sizeof(str_header)); oledDrawString(offset_x, offset_y, str_header, FONT_STANDARD); offset_y += line_height; // Invert color on header oledInvert(0, 0, OLED_WIDTH, offset_y - 2); // Dialog contents begin if (line1) { oledDrawString(offset_x, offset_y, line1, FONT_STANDARD); } offset_y += line_height; if (line2) { oledDrawString(offset_x, offset_y, line2, FONT_STANDARD); } offset_y += line_height; if (line3) { oledDrawString(offset_x, offset_y, line3, FONT_STANDARD); } offset_y += line_height; if (line4) { oledDrawString(offset_x, offset_y, line4, FONT_STANDARD); } offset_y += line_height; if (line5) { oledDrawString(offset_x, offset_y, line5, FONT_STANDARD); } offset_y += line_height; // Cancel button layoutButtonNo(_("Cancel"), &bmp_btn_cancel); // Warnings (drawn centered between the buttons if (warning) { oledDrawStringCenter(OLED_WIDTH / 2, OLED_HEIGHT - 8, warning, FONT_STANDARD); } // Next / sign button char str_next_label[8] = {0}; if (is_final_step) { strlcpy(str_next_label, _("SIGN"), sizeof(str_next_label)); } else { strlcpy(str_next_label, _("Next"), sizeof(str_next_label)); } layoutButtonYes(str_next_label, &bmp_btn_confirm); oledRefresh(); } /* * Main dialog helper method. Allows displaying 5 lines. * A title showing the account being used to sign is always displayed. */ void stellar_layoutTransactionDialog(const char *line1, const char *line2, const char *line3, const char *line4, const char *line5) { char str_warning[16] = {0}; memzero(str_warning, sizeof(str_warning)); if (stellar_activeTx.network_type == 2) { // Warning: testnet strlcpy(str_warning, _("WRN:TN"), sizeof(str_warning)); } if (stellar_activeTx.network_type == 3) { // Warning: private network strlcpy(str_warning, _("WRN:PN"), sizeof(str_warning)); } stellar_layoutSigningDialog( line1, line2, line3, line4, line5, stellar_activeTx.address_n, stellar_activeTx.address_n_count, str_warning, false); }