cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
eth_addr.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
11
12#include "eth_addr.h"
13
14#include <string.h>
15#include <stdbool.h>
16
17#include "keccak256.h"
18#include "CW_Utils.h" /* SDK hardened primitives: secure_wipe */
19
26static int hex_nibble_val(char c)
27{
28 if ((c >= '0') && (c <= '9')) { return c - '0'; }
29 if ((c >= 'a') && (c <= 'f')) { return (c - 'a') + 10; }
30 if ((c >= 'A') && (c <= 'F')) { return (c - 'A') + 10; }
31 return -1;
32}
33
34bool eth_addr_parse(const char *hex, uint8_t out[ETH_ADDR_LEN])
35{
36 const char *p = hex;
37 if ((p[0] == '0') && ((p[1] == 'x') || (p[1] == 'X'))) {
38 p += 2;
39 }
40 if (strlen(p) != ETH_ADDR_HEX_LEN) {
41 return false;
42 }
43
44 CW_Utils::secure_wipe(out, ETH_ADDR_LEN);
45
46 /* Single pass: validate every nibble, decode the bytes, lower-case a copy
47 * for the checksum hash, and note whether the source is mixed-case. */
48 char lower[ETH_ADDR_HEX_LEN];
49 bool has_upper = false;
50 bool has_lower = false;
51 size_t i;
52 for (i = 0U; i < ETH_ADDR_HEX_LEN; i++) {
53 char c = p[i];
54 int v = hex_nibble_val(c);
55 if (v < 0) {
56 return false;
57 }
58 if ((c >= 'a') && (c <= 'f')) {
59 has_lower = true;
60 lower[i] = c;
61 } else if ((c >= 'A') && (c <= 'F')) {
62 has_upper = true;
63 lower[i] = static_cast<char>((c - 'A') + 'a');
64 } else {
65 lower[i] = c; /* digit — case-neutral */
66 }
67 if ((i & 1U) == 0U) {
68 out[i / 2U] = static_cast<uint8_t>(static_cast<uint8_t>(v) << 4U);
69 } else {
70 out[i / 2U] |= static_cast<uint8_t>(v);
71 }
72 }
73
74 /* The zero address is nobody's: a transfer to it burns the funds, and an
75 * all-zero string is what a blank or wiped setting decodes to. It is never
76 * a payee or a contract this terminal should act on. */
77 uint8_t any = 0U;
78 for (i = 0U; i < ETH_ADDR_LEN; i++) { any |= out[i]; }
79 if (any == 0U) {
80 return false;
81 }
82
83 /* EIP-55: a mixed-case address carries a checksum, so verify it; an
84 * all-lower or all-upper address is treated as un-checksummed and accepted
85 * (standard lenient behaviour). A checksummed ADDR_TO thus rejects any
86 * single-nibble corruption or typo of the source string. */
87 if (has_upper && has_lower) {
88 uint8_t h[32];
89 keccak256(reinterpret_cast<const uint8_t *>(lower), ETH_ADDR_HEX_LEN, h);
90 for (i = 0U; i < ETH_ADDR_HEX_LEN; i++) {
91 char c = p[i];
92 bool is_alpha = ((c >= 'a') && (c <= 'f')) ||
93 ((c >= 'A') && (c <= 'F'));
94 if (!is_alpha) {
95 continue; /* digits have no case to check */
96 }
97 uint8_t nib = ((i & 1U) == 0U) ? static_cast<uint8_t>(h[i / 2U] >> 4U)
98 : static_cast<uint8_t>(h[i / 2U] & 0x0FU);
99 bool want_upper = (nib >= 8U);
100 bool is_upper = ((c >= 'A') && (c <= 'F'));
101 if (want_upper != is_upper) {
102 return false;
103 }
104 }
105 }
106 return true;
107}
108
109bool eth_addr_format(const uint8_t addr[ETH_ADDR_LEN], char *out, size_t n)
110{
111 /* "0x" + 40 hex + NUL */
112 if ((out == NULL) || (n < (ETH_ADDR_HEX_LEN + 3U))) {
113 if ((out != NULL) && (n > 0U)) { out[0] = '\0'; }
114 return false;
115 }
116
117 static const char LOWER[] = "0123456789abcdef";
118
119 /* Lower-case hex first: EIP-55 hashes exactly that string, so it has to
120 * exist before any character can be given its case. */
121 char lower[ETH_ADDR_HEX_LEN];
122 size_t i;
123 for (i = 0U; i < ETH_ADDR_LEN; i++) {
124 lower[i * 2U] = LOWER[(addr[i] >> 4U) & 0x0FU];
125 lower[(i * 2U) + 1U] = LOWER[addr[i] & 0x0FU];
126 }
127
128 uint8_t h[32];
129 keccak256(reinterpret_cast<const uint8_t *>(lower), ETH_ADDR_HEX_LEN, h);
130
131 out[0] = '0';
132 out[1] = 'x';
133 for (i = 0U; i < ETH_ADDR_HEX_LEN; i++) {
134 char c = lower[i];
135 if ((c >= 'a') && (c <= 'f')) {
136 const uint8_t nib = ((i & 1U) == 0U)
137 ? static_cast<uint8_t>(h[i / 2U] >> 4U)
138 : static_cast<uint8_t>(h[i / 2U] & 0x0FU);
139 if (nib >= 8U) { c = static_cast<char>((c - 'a') + 'A'); }
140 }
141 out[i + 2U] = c;
142 }
143 out[ETH_ADDR_HEX_LEN + 2U] = '\0';
144 return true;
145}
static int hex_nibble_val(char c)
Decode a single ASCII hex digit.
Definition eth_addr.cpp:26
bool eth_addr_parse(const char *hex, uint8_t out[ETH_ADDR_LEN])
Parse a 20-byte Ethereum address from a hex string.
Definition eth_addr.cpp:34
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
Hex Ethereum-address parsing — a pure, dependency-free unit so it can be unit-tested and fuzzed on th...
#define ETH_ADDR_HEX_LEN
Length of the same address in hex characters (no 0x prefix).
Definition eth_addr.h:34
#define ETH_ADDR_LEN
Length of a raw (binary) Ethereum address, in bytes.
Definition eth_addr.h:25
void keccak256(const uint8_t *input, size_t length, uint8_t digest[32])
Compute the Keccak-256 digest of a buffer.
Original Keccak-256 digest as used by Ethereum.