cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
card_io.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
12
13#include "pos_app.h"
14
15/* Why the card just tapped was refused, or NULL. Set by card_connect(), read by
16 * its callers in place of a generic "Card not found". Plain static: written and
17 * read only on the main task. */
18const char *s_card_fault = NULL;
19
30static const char *card_fault(Pn532NfcTransport &transport)
31{
32 static const uint8_t SELECT[] = CARD_SELECT_APDU;
33 uint8_t r[40];
34 uint8_t n = static_cast<uint8_t>(sizeof(r));
35 if (!transport.sendAPDU(SELECT, static_cast<uint8_t>(sizeof(SELECT)), r, n)) {
36 return NULL; /* card gone or not answering — the caller's own story */
37 }
38 const card_state_t st = card_state(r, n);
39 if (st != CARD_READY) {
40 ESP_LOGW(TAG, "card refused: state %d", static_cast<int>(st));
41 }
42 return card_state_text(st);
43}
44
52const char *pin_fail_text(Pn532NfcTransport &transport, const char *wrong)
53{
54 static const uint8_t SELECT[] = CARD_SELECT_APDU;
55 uint8_t r[40];
56 uint8_t n = static_cast<uint8_t>(sizeof(r));
57 if (!transport.sendAPDU(SELECT, static_cast<uint8_t>(sizeof(SELECT)), r, n)) {
58 ESP_LOGW(TAG, "verifyPin failed and the card is gone - not a PIN error");
59 return "Card moved - tap it again";
60 }
61 return wrong;
62}
63
79bool card_connect(CryptnoxWallet &wallet, Pn532NfcTransport &transport,
80 CW_SecureSession &session, bool setup)
81{
82 s_card_fault = NULL;
83 if (!s_user_cancelled) {
84 if (setup) { ui_show_card_wait("Reading your payout addresses"); }
85 else { ui_show_tx_status(UI_TX_STATE_PLACE_CARD, "Hold card to reader"); }
86 }
87
88 /* Start from a clean reader state: a previous attempt that ended with the
89 * card ripped away mid-exchange can leave a stale target selected, which
90 * would make every InListPassiveTarget below time out. */
91 transport.resetReader();
92
93 /* Give the user up to 60 s to present the card. */
94 const int64_t card_wait_us = 60LL * 1000000LL;
95 const int64_t start_us = esp_timer_get_time();
96 while (true) {
97 wdt_feed(); /* a minute of waiting for a card is on purpose */
98 if (s_user_cancelled) {
99 return false;
100 }
101 if ((esp_timer_get_time() - start_us) > card_wait_us) {
102 return false; /* timed out waiting for the card */
103 }
104 if (transport.inListPassiveTarget()) {
105 /* Card tapped — show immediate feedback while the secure channel
106 * comes up. */
107 if (setup) { ui_show_card_wait("Reading..."); }
109 vTaskDelay(pdMS_TO_TICKS(200));
110 /* Before the channel: an uninitialised card would otherwise fail at
111 * the PIN and send the holder off to retype it. */
112 s_card_fault = card_fault(transport);
113 if (s_card_fault != NULL) {
114 transport.resetReader();
115 return false;
116 }
117 if (wallet.establishSecureChannel(session)) {
118 return true;
119 }
120 /* Card pulled away or channel failed: release the dead target, or
121 * every following InListPassiveTarget times out. */
122 transport.resetReader();
123 if (!s_user_cancelled) {
124 if (setup) { ui_show_card_wait("Hold it still"); }
125 else { ui_show_tx_status(UI_TX_STATE_PLACE_CARD, "Hold card to reader"); }
126 }
127 }
128 vTaskDelay(pdMS_TO_TICKS(200));
129 }
130}
131
160bool card_sign(CryptnoxWallet &wallet, CW_SecureSession &session,
161 const uint8_t *hash, uint8_t hash_len,
162 const uint8_t *path, uint8_t path_len,
163 const char *pin, size_t pin_chars,
164 uint8_t rs_out[64], char *err_out, size_t err_max)
165{
166 CW_SignRequest req(session,
167 CW_SIGN_DERIVE_K1,
168 CW_SIGN_SIG_ECDSA_LOW_S,
169 CW_SIGN_WITH_PIN);
170 req.hash = hash;
171 req.hashLength = hash_len;
172 req.derivePath = path;
173 req.derivePathLength = path_len;
174
175 /* Copy the operator-entered PIN into the request as late as possible;
176 * req.pin is zero-initialised by the CW_SignRequest constructor. */
177 const size_t copy_len = (pin_chars < CW_MAX_PIN_LENGTH) ? pin_chars
178 : CW_MAX_PIN_LENGTH;
179 (void)CW_Utils::safe_memcpy(req.pin, sizeof(req.pin),
180 reinterpret_cast<const uint8_t *>(pin),
181 copy_len);
182
183 CW_SignResult result = wallet.sign(req);
184 WipeGuard g_sig(result.signature, sizeof(result.signature));
185 wallet.disconnect(session);
186
187 /* scrub the PIN immediately after use (secure_wipe is not
188 * dead-store-eliminated); ~CW_SignRequest wipes it again as a backstop. */
189 CW_Utils::secure_wipe(req.pin, sizeof(req.pin));
190
191 if (result.errorCode != CW_OK) {
192 /* SIGN carries the PIN (CW_SIGN_WITH_PIN), so a mistyped one arrives
193 * here as a status byte; name it rather than show "Sign error 0x82". */
194 if (result.errorCode == CW_SIGN_PIN_INCORRECT) {
195 (void)snprintf(err_out, err_max, "Wrong PIN");
196 } else {
197 (void)snprintf(err_out, err_max, "Sign error 0x%02X",
198 static_cast<unsigned int>(result.errorCode));
199 }
200 return false;
201 }
202
203 (void)CW_Utils::safe_memcpy(rs_out, 32U,
204 result.signature + CW_SIG_R_OFFSET, 32U);
205 (void)CW_Utils::safe_memcpy(rs_out + 32U, 32U,
206 result.signature + CW_SIG_S_OFFSET, 32U);
207 return true;
208}
209
233bool card_read_payouts(CryptnoxWallet &wallet,
234 Pn532NfcTransport &transport,
235 CW_CryptoProvider &crypto,
236 const char *pin, size_t pin_chars,
237 char *eth_out, size_t eth_n,
238 char *tron_out, size_t tron_n,
239 char *err, size_t err_n)
240{
241 eth_out[0] = '\0';
242 tron_out[0] = '\0';
243
244 CW_SecureSession session;
245 /* setup = true: same 60-second cancellable poll, but the card-wait screen
246 * rather than the transaction one. Nothing is being paid here. */
247 if (!card_connect(wallet, transport, session, true)) {
248 (void)snprintf(err, err_n, "%s",
249 s_user_cancelled ? "Cancelled" :
250 (s_card_fault != NULL) ? s_card_fault
251 : "Card not found");
252 return false;
253 }
254
255 if (!wallet.verifyPin(session, reinterpret_cast<const uint8_t *>(pin),
256 static_cast<uint8_t>(pin_chars))) {
257 (void)snprintf(err, err_n, "%s", pin_fail_text(transport, "Wrong card PIN"));
258 wallet.disconnect(session);
259 return false;
260 }
261
262 uint8_t pubkey[64];
263 WipeGuard g_pub(pubkey, sizeof(pubkey));
264
265 /* Ethereum: keccak256 of the uncompressed public key, low 20 bytes, rendered
266 * checksummed so it matches what the operator's own wallet shows. */
267 if (wallet.getPublicKey(session, ETH_DERIVE_PATH,
268 static_cast<uint8_t>(sizeof(ETH_DERIVE_PATH)),
269 pubkey)) {
270 uint8_t h[32];
271 keccak256(pubkey, sizeof(pubkey), h);
272 (void)eth_addr_format(&h[12], eth_out, eth_n);
273 CW_Utils::secure_wipe(h, sizeof(h));
274 } else {
275 ESP_LOGW(TAG, "card would not export its Ethereum key");
276 }
277
278 uint8_t addr21[CW_TRON_ADDRESS_BYTES];
279 if (wallet.getPublicKey(session, CW_TRON_DERIVE_PATH,
280 CW_TRON_PATH_LENGTH, pubkey) &&
281 CW_Tron::addressBytesFromPublicKey(pubkey, addr21)) {
282 (void)CW_Tron::encodeAddress(addr21, crypto, tron_out, tron_n);
283 } else {
284 ESP_LOGW(TAG, "card would not export its Tron key");
285 }
286
287 wallet.disconnect(session);
288
289 if ((eth_out[0] == '\0') && (tron_out[0] == '\0')) {
290 (void)snprintf(err, err_n, "Card gave no usable address");
291 return false;
292 }
293 ESP_LOGI(TAG, "card addresses: eth=%s tron=%s", eth_out, tron_out);
294 return true;
295}
bool card_sign(CryptnoxWallet &wallet, CW_SecureSession &session, const uint8_t *hash, uint8_t hash_len, const uint8_t *path, uint8_t path_len, const char *pin, size_t pin_chars, uint8_t rs_out[64], char *err_out, size_t err_max)
Have the card sign hash, then close the session.
Definition card_io.cpp:160
const char * s_card_fault
Definition card_io.cpp:18
bool card_read_payouts(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_CryptoProvider &crypto, const char *pin, size_t pin_chars, char *eth_out, size_t eth_n, char *tron_out, size_t tron_n, char *err, size_t err_n)
Read the card's payout addresses and put them through the panel.
Definition card_io.cpp:233
bool card_connect(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_SecureSession &session, bool setup)
Wait for a card and open a secure channel, cancellable from the UI.
Definition card_io.cpp:79
static const char * card_fault(Pn532NfcTransport &transport)
Whether the card now on the reader is set up at all.
Definition card_io.cpp:30
const char * pin_fail_text(Pn532NfcTransport &transport, const char *wrong)
Why verifyPin said no: the PIN, or the card leaving the field.
Definition card_io.cpp:52
card_state_t
What a tapped card is missing.
Definition card_status.h:52
@ CARD_READY
Definition card_status.h:53
static const char * card_state_text(card_state_t s)
The line to put in front of the operator, or NULL if there is none.
Definition card_status.h:94
#define CARD_SELECT_APDU
SELECT for the Cryptnox applet: CLA INS P1 P2 Lc || AID.
Definition card_status.h:39
static card_state_t card_state(const uint8_t *r, size_t n)
Judge a card from its SELECT response.
Definition card_status.h:70
bool eth_addr_format(const uint8_t addr[ETH_ADDR_LEN], char *out, size_t n)
Render a 20-byte address as an EIP-55 mixed-case "0x..." string.
Definition eth_addr.cpp:109
static const char *const TAG
Definition eth_rpc.cpp:33
void keccak256(const uint8_t *input, size_t length, uint8_t digest[32])
Compute the Keccak-256 digest of a buffer.
std::atomic< bool > s_user_cancelled
Definition main.cpp:27
const uint8_t ETH_DERIVE_PATH[20]
Definition pay.cpp:16
Private to the application files (main, boot, card_io, pay, pay_evm, pay_tron): the state they share ...
Scrubs a buffer with CW_Utils::secure_wipe when it leaves scope.
Definition pos_app.h:215
void ui_show_tx_status(ui_tx_state_t state, const char *info)
Switch to the transaction-status screen.
Definition ui.cpp:898
void ui_show_card_wait(const char *note)
"Hold your card to the reader" while the address is read.
Definition ui.cpp:886
@ UI_TX_STATE_PLACE_CARD
Definition ui.h:95
@ UI_TX_STATE_PROCESSING
Definition ui.h:96
static void wdt_feed(void)
Feed the task watchdog if, and only if, the calling task is subscribed.
Definition wdt.h:28