v0.0.29 - Feather firmware: auto-approve sign_event and always return explicit JSON-RPC errors for unhandled/oversized requests
This commit is contained in:
BIN
firmware/feather_s3_tft/esp32-s3-reverse-tft-feather.pdf
Normal file
BIN
firmware/feather_s3_tft/esp32-s3-reverse-tft-feather.pdf
Normal file
Binary file not shown.
@@ -6,8 +6,20 @@ idf_component_register(
|
||||
"key_derivation.c"
|
||||
"bech32.c"
|
||||
"usb_transport.c"
|
||||
"buttons.c"
|
||||
"ui.c"
|
||||
"secure_mem.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nip004.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nip044.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nostr_common.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/utils.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_aes.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_chacha20.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_secp256k1.c"
|
||||
"../../../resources/nostr_core_lib/platform/esp32/nostr_platform_esp32.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"../../../resources/nostr_core_lib/nostr_core"
|
||||
REQUIRES
|
||||
mbedtls
|
||||
secp256k1
|
||||
|
||||
155
firmware/feather_s3_tft/main/buttons.c
Normal file
155
firmware/feather_s3_tft/main/buttons.c
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "buttons.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define BTN_D0_GPIO 0 /* D0/BOOT, active LOW */
|
||||
#define BTN_D1_GPIO 1 /* D1, active HIGH */
|
||||
#define BTN_D2_GPIO 2 /* D2, active HIGH */
|
||||
|
||||
#define BTN_DEBOUNCE_MS 30
|
||||
#define BTN_LONG_MS 1000
|
||||
#define BTN_POLL_MS 10
|
||||
|
||||
static bool s_buttons_inited = false;
|
||||
|
||||
static bool button_pressed(btn_id_t id)
|
||||
{
|
||||
if (id == BTN_D0) {
|
||||
return gpio_get_level(BTN_D0_GPIO) == 0;
|
||||
}
|
||||
if (id == BTN_D1) {
|
||||
return gpio_get_level(BTN_D1_GPIO) == 1;
|
||||
}
|
||||
if (id == BTN_D2) {
|
||||
return gpio_get_level(BTN_D2_GPIO) == 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool any_pressed(void)
|
||||
{
|
||||
return button_pressed(BTN_D0) || button_pressed(BTN_D1) || button_pressed(BTN_D2);
|
||||
}
|
||||
|
||||
void buttons_init(void)
|
||||
{
|
||||
if (s_buttons_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_config_t d0_cfg = {
|
||||
.pin_bit_mask = 1ULL << BTN_D0_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&d0_cfg);
|
||||
|
||||
gpio_config_t d1d2_cfg = {
|
||||
.pin_bit_mask = (1ULL << BTN_D1_GPIO) | (1ULL << BTN_D2_GPIO),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_ENABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&d1d2_cfg);
|
||||
|
||||
s_buttons_inited = true;
|
||||
}
|
||||
|
||||
btn_event_t buttons_poll(void)
|
||||
{
|
||||
btn_event_t evt = {.id = BTN_NONE, .kind = BTN_EVT_NONE};
|
||||
|
||||
if (!s_buttons_inited) {
|
||||
buttons_init();
|
||||
}
|
||||
|
||||
if (button_pressed(BTN_D0)) {
|
||||
evt.id = BTN_D0;
|
||||
evt.kind = BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
if (button_pressed(BTN_D1)) {
|
||||
evt.id = BTN_D1;
|
||||
evt.kind = BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
if (button_pressed(BTN_D2)) {
|
||||
evt.id = BTN_D2;
|
||||
evt.kind = BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
|
||||
return evt;
|
||||
}
|
||||
|
||||
btn_event_t buttons_wait(uint32_t timeout_ms)
|
||||
{
|
||||
btn_event_t evt = {.id = BTN_NONE, .kind = BTN_EVT_NONE};
|
||||
uint32_t elapsed = 0;
|
||||
const bool wait_forever = (timeout_ms == 0xFFFFFFFFu);
|
||||
|
||||
if (!s_buttons_inited) {
|
||||
buttons_init();
|
||||
}
|
||||
|
||||
while (any_pressed()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_POLL_MS));
|
||||
if (!wait_forever) {
|
||||
elapsed += BTN_POLL_MS;
|
||||
if (elapsed >= timeout_ms) {
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (wait_forever || elapsed < timeout_ms) {
|
||||
btn_id_t id = BTN_NONE;
|
||||
|
||||
if (button_pressed(BTN_D0)) {
|
||||
id = BTN_D0;
|
||||
} else if (button_pressed(BTN_D1)) {
|
||||
id = BTN_D1;
|
||||
} else if (button_pressed(BTN_D2)) {
|
||||
id = BTN_D2;
|
||||
}
|
||||
|
||||
if (id != BTN_NONE) {
|
||||
uint32_t held_ms = 0;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_DEBOUNCE_MS));
|
||||
if (!button_pressed(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (button_pressed(id)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_POLL_MS));
|
||||
held_ms += BTN_POLL_MS;
|
||||
if (!wait_forever) {
|
||||
elapsed += BTN_POLL_MS;
|
||||
if (elapsed >= timeout_ms) {
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_DEBOUNCE_MS));
|
||||
evt.id = id;
|
||||
evt.kind = (held_ms >= BTN_LONG_MS) ? BTN_EVT_LONG_PRESS : BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_POLL_MS));
|
||||
if (!wait_forever) {
|
||||
elapsed += BTN_POLL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
return evt;
|
||||
}
|
||||
33
firmware/feather_s3_tft/main/buttons.h
Normal file
33
firmware/feather_s3_tft/main/buttons.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
BTN_NONE = 0,
|
||||
BTN_D0,
|
||||
BTN_D1,
|
||||
BTN_D2,
|
||||
} btn_id_t;
|
||||
|
||||
typedef enum {
|
||||
BTN_EVT_NONE = 0,
|
||||
BTN_EVT_PRESS,
|
||||
BTN_EVT_LONG_PRESS,
|
||||
} btn_event_kind_t;
|
||||
|
||||
typedef struct {
|
||||
btn_id_t id;
|
||||
btn_event_kind_t kind;
|
||||
} btn_event_t;
|
||||
|
||||
void buttons_init(void);
|
||||
btn_event_t buttons_wait(uint32_t timeout_ms);
|
||||
btn_event_t buttons_poll(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -41,6 +41,9 @@
|
||||
#define CHAR_SCALE 2
|
||||
#define CHAR_W (6 * CHAR_SCALE)
|
||||
#define CHAR_H (8 * CHAR_SCALE)
|
||||
#define CHAR_SCALE_SMALL 1
|
||||
#define CHAR_W_SMALL (6 * CHAR_SCALE_SMALL)
|
||||
#define CHAR_H_SMALL (8 * CHAR_SCALE_SMALL)
|
||||
|
||||
static spi_device_handle_t s_lcd = NULL;
|
||||
static bool s_initialized = false;
|
||||
@@ -238,12 +241,16 @@ esp_err_t display_clear(uint16_t color)
|
||||
return display_fill_rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, color);
|
||||
}
|
||||
|
||||
static esp_err_t draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)
|
||||
static esp_err_t draw_char_scaled(int x, int y, char c, int scale, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (c < 0x20 || c > 0x7F) {
|
||||
c = '?';
|
||||
}
|
||||
|
||||
if (scale <= 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const uint8_t *glyph = font5x7[(uint8_t)c - 0x20];
|
||||
|
||||
for (int col = 0; col < 5; ++col) {
|
||||
@@ -251,28 +258,30 @@ static esp_err_t draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)
|
||||
for (int row = 0; row < 7; ++row) {
|
||||
bool on = (bits >> row) & 0x1;
|
||||
ESP_RETURN_ON_ERROR(
|
||||
display_fill_rect(x + (col * CHAR_SCALE), y + (row * CHAR_SCALE), CHAR_SCALE, CHAR_SCALE, on ? fg : bg),
|
||||
display_fill_rect(x + (col * scale), y + (row * scale), scale, scale, on ? fg : bg),
|
||||
TAG,
|
||||
"pixel draw failed");
|
||||
}
|
||||
}
|
||||
|
||||
// spacing column + bottom row
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x + (5 * CHAR_SCALE), y, CHAR_SCALE, 7 * CHAR_SCALE, bg), TAG, "spacing failed");
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y + (7 * CHAR_SCALE), 6 * CHAR_SCALE, CHAR_SCALE, bg), TAG, "baseline failed");
|
||||
/* spacing column + bottom row */
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x + (5 * scale), y, scale, 7 * scale, bg), TAG, "spacing failed");
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y + (7 * scale), 6 * scale, scale, bg), TAG, "baseline failed");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
static esp_err_t draw_text_scaled(int x, int y, const char *text, int scale, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (!text) {
|
||||
if (!text || scale <= 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const int char_w = 6 * scale;
|
||||
const int char_h = 8 * scale;
|
||||
int cursor_x = x;
|
||||
int cursor_y = y;
|
||||
|
||||
@@ -280,25 +289,79 @@ esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_
|
||||
char c = text[i];
|
||||
if (c == '\n') {
|
||||
cursor_x = x;
|
||||
cursor_y += CHAR_H;
|
||||
cursor_y += char_h;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cursor_x + CHAR_W > DISPLAY_WIDTH) {
|
||||
if (cursor_x + char_w > DISPLAY_WIDTH) {
|
||||
cursor_x = x;
|
||||
cursor_y += CHAR_H;
|
||||
cursor_y += char_h;
|
||||
}
|
||||
if (cursor_y + CHAR_H > DISPLAY_HEIGHT) {
|
||||
if (cursor_y + char_h > DISPLAY_HEIGHT) {
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(draw_char(cursor_x, cursor_y, c, fg, bg), TAG, "draw_char failed");
|
||||
cursor_x += CHAR_W;
|
||||
ESP_RETURN_ON_ERROR(draw_char_scaled(cursor_x, cursor_y, c, scale, fg, bg), TAG, "draw_char failed");
|
||||
cursor_x += char_w;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
return draw_text_scaled(x, y, text, CHAR_SCALE, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
return draw_text_scaled(x, y, text, CHAR_SCALE_SMALL, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int x;
|
||||
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
x = (DISPLAY_WIDTH - ((int)strlen(text) * CHAR_W)) / 2;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return display_draw_text(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int x;
|
||||
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
x = (DISPLAY_WIDTH - ((int)strlen(text) * CHAR_W_SMALL)) / 2;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return display_draw_text_small(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg)
|
||||
{
|
||||
int w;
|
||||
int h = CHAR_H;
|
||||
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
w = (int)strlen(text) * CHAR_W;
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y, w, h, bg), TAG, "inverse bg fill failed");
|
||||
return display_draw_text(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_button_status(int btn_id, bool pressed)
|
||||
{
|
||||
if (btn_id < 0 || btn_id > 2) {
|
||||
|
||||
@@ -19,11 +19,16 @@ extern "C" {
|
||||
#define RGB565_YELLOW 0xFFE0
|
||||
#define RGB565_CYAN 0x07FF
|
||||
#define RGB565_MAGENTA 0xF81F
|
||||
#define RGB565_DIM_GRAY 0x4208
|
||||
|
||||
esp_err_t display_init(void);
|
||||
esp_err_t display_clear(uint16_t color);
|
||||
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color);
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg);
|
||||
esp_err_t display_draw_button_status(int btn_id, bool pressed);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -139,14 +139,14 @@ static int bip32_ckd_priv(const secp256k1_context *ctx,
|
||||
return compute_compressed_pubkey(ctx, child->priv, child->pub);
|
||||
}
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32])
|
||||
static int derive_nostr_key_internal(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
static const uint32_t path[5] = {
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
1237u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u,
|
||||
0u,
|
||||
nostr_index,
|
||||
};
|
||||
|
||||
secp256k1_context *ctx = NULL;
|
||||
@@ -212,6 +212,16 @@ int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey
|
||||
return 0;
|
||||
}
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
return derive_nostr_key_internal(seed, 0u, privkey, pubkey);
|
||||
}
|
||||
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
return derive_nostr_key_internal(seed, nostr_index, privkey, pubkey);
|
||||
}
|
||||
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64])
|
||||
{
|
||||
secp256k1_context *ctx = NULL;
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
#include <stdint.h>
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/gpio.h"
|
||||
|
||||
#include "mbedtls/sha256.h"
|
||||
#include "cJSON.h"
|
||||
#include "secp256k1.h"
|
||||
@@ -16,10 +14,16 @@
|
||||
#include "secp256k1_schnorrsig.h"
|
||||
|
||||
#include "bech32.h"
|
||||
#include "buttons.h"
|
||||
#include "display.h"
|
||||
#include "key_derivation.h"
|
||||
#include "mnemonic.h"
|
||||
#include "usb_transport.h"
|
||||
#include "ui.h"
|
||||
#include "secure_mem.h"
|
||||
#include "nip004.h"
|
||||
#include "nip044.h"
|
||||
#include "nostr_common.h"
|
||||
|
||||
#define FIRMWARE_VERSION "0.0.1"
|
||||
#ifndef GIT_HASH
|
||||
@@ -28,12 +32,7 @@
|
||||
|
||||
static const char *TAG = "nsigner";
|
||||
|
||||
#define BTN_DENY_GPIO 0 /* D0/BOOT, active LOW */
|
||||
#define BTN_APPROVE_GPIO 1 /* D1, active HIGH */
|
||||
#define BTN_ALWAYS_GPIO 2 /* D2, active HIGH */
|
||||
|
||||
#define APPROVAL_TIMEOUT_MS 30000
|
||||
#define APPROVAL_DEBOUNCE_MS 30
|
||||
|
||||
typedef enum {
|
||||
APPROVAL_DENY = 0,
|
||||
@@ -42,7 +41,6 @@ typedef enum {
|
||||
APPROVAL_TIMEOUT = 3,
|
||||
} approval_decision_t;
|
||||
|
||||
static bool s_buttons_inited = false;
|
||||
static bool s_always_allow_sign = false;
|
||||
|
||||
#define AUTH_REQUIRED 1
|
||||
@@ -72,89 +70,29 @@ static char s_pubkey_hex[65];
|
||||
static char s_response_buf[2048];
|
||||
static char s_signed_event_buf[1600];
|
||||
|
||||
static char s_encrypt_buf[1600];
|
||||
|
||||
static int transport_init(void)
|
||||
{
|
||||
return usb_transport_init();
|
||||
}
|
||||
|
||||
static void buttons_init(void)
|
||||
{
|
||||
if (s_buttons_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_config_t deny_cfg = {
|
||||
.pin_bit_mask = 1ULL << BTN_DENY_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&deny_cfg);
|
||||
|
||||
gpio_config_t approve_cfg = {
|
||||
.pin_bit_mask = (1ULL << BTN_APPROVE_GPIO) | (1ULL << BTN_ALWAYS_GPIO),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_ENABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&approve_cfg);
|
||||
|
||||
s_buttons_inited = true;
|
||||
}
|
||||
|
||||
static bool button_pressed_deny(void)
|
||||
{
|
||||
return gpio_get_level(BTN_DENY_GPIO) == 0;
|
||||
}
|
||||
|
||||
static bool button_pressed_approve(void)
|
||||
{
|
||||
return gpio_get_level(BTN_APPROVE_GPIO) == 1;
|
||||
}
|
||||
|
||||
static bool button_pressed_always(void)
|
||||
{
|
||||
return gpio_get_level(BTN_ALWAYS_GPIO) == 1;
|
||||
}
|
||||
|
||||
static approval_decision_t wait_for_approval(uint32_t timeout_ms)
|
||||
{
|
||||
uint32_t elapsed = 0;
|
||||
const uint32_t step = 20;
|
||||
btn_event_t evt = buttons_wait(timeout_ms);
|
||||
|
||||
/* Drain any currently-held button before sampling fresh presses. */
|
||||
while (button_pressed_deny() || button_pressed_approve() || button_pressed_always()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
elapsed += 10;
|
||||
if (elapsed >= timeout_ms) {
|
||||
return APPROVAL_TIMEOUT;
|
||||
}
|
||||
if (evt.kind == BTN_EVT_NONE) {
|
||||
return APPROVAL_TIMEOUT;
|
||||
}
|
||||
|
||||
while (elapsed < timeout_ms) {
|
||||
if (button_pressed_deny()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(APPROVAL_DEBOUNCE_MS));
|
||||
if (button_pressed_deny()) {
|
||||
return APPROVAL_DENY;
|
||||
}
|
||||
}
|
||||
if (button_pressed_always()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(APPROVAL_DEBOUNCE_MS));
|
||||
if (button_pressed_always()) {
|
||||
return APPROVAL_ALWAYS;
|
||||
}
|
||||
}
|
||||
if (button_pressed_approve()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(APPROVAL_DEBOUNCE_MS));
|
||||
if (button_pressed_approve()) {
|
||||
return APPROVAL_ONCE;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(step));
|
||||
elapsed += step;
|
||||
if (evt.id == BTN_D0) {
|
||||
return APPROVAL_DENY;
|
||||
}
|
||||
if (evt.id == BTN_D1) {
|
||||
return APPROVAL_ONCE;
|
||||
}
|
||||
if (evt.id == BTN_D2) {
|
||||
return APPROVAL_ALWAYS;
|
||||
}
|
||||
|
||||
return APPROVAL_TIMEOUT;
|
||||
@@ -189,24 +127,24 @@ static void show_approval_prompt_with_caller(const cJSON *event_in, const char *
|
||||
snprintf(line_content, sizeof(line_content), "%.50s", content);
|
||||
|
||||
display_clear(RGB565_BLACK);
|
||||
display_draw_text(10, 10, "APPROVE sign_event?", RGB565_YELLOW, RGB565_BLACK);
|
||||
display_draw_text(10, 24, line_caller, RGB565_MAGENTA, RGB565_BLACK);
|
||||
display_draw_text(10, 10, "APPROVE sign_event?", RGB565_RED, RGB565_BLACK);
|
||||
display_draw_text(10, 24, line_caller, RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 40, line_kind, RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 56, line_content, RGB565_CYAN, RGB565_BLACK);
|
||||
display_draw_text(10, 80, "D1=allow once", RGB565_GREEN, RGB565_BLACK);
|
||||
display_draw_text(10, 96, "D2=always allow", RGB565_GREEN, RGB565_BLACK);
|
||||
display_draw_text(10, 112, "D0=deny", RGB565_RED, RGB565_BLACK);
|
||||
display_draw_text(10, 56, line_content, RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 80, "D1=allow once", RGB565_DIM_GRAY, RGB565_BLACK);
|
||||
display_draw_text(10, 96, "D2=always allow", RGB565_DIM_GRAY, RGB565_BLACK);
|
||||
display_draw_text(10, 112, "D0=deny", RGB565_DIM_GRAY, RGB565_BLACK);
|
||||
}
|
||||
|
||||
static void show_idle_screen(void)
|
||||
{
|
||||
display_clear(RGB565_BLACK);
|
||||
display_draw_text(10, 10, "nostr npub", RGB565_GREEN, RGB565_BLACK);
|
||||
display_draw_text(10, 30, s_npub_short, RGB565_GREEN, RGB565_BLACK);
|
||||
display_draw_text(10, 10, "nostr npub", RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 30, s_npub_short, RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 50, "mn:", RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(46, 50, s_mnemonic_short, RGB565_YELLOW, RGB565_BLACK);
|
||||
display_draw_text(10, 70, "v" FIRMWARE_VERSION, RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 90, "ready", RGB565_CYAN, RGB565_BLACK);
|
||||
display_draw_text(46, 50, s_mnemonic_short, RGB565_WHITE, RGB565_BLACK);
|
||||
display_draw_text(10, 70, "v" FIRMWARE_VERSION, RGB565_DIM_GRAY, RGB565_BLACK);
|
||||
display_draw_text(10, 90, "ready", RGB565_DIM_GRAY, RGB565_BLACK);
|
||||
}
|
||||
|
||||
static int recv_frame_stdin(uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
@@ -761,93 +699,143 @@ done:
|
||||
return rc;
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
static int parse_nostr_index_from_params(cJSON *params, uint32_t *out_index)
|
||||
{
|
||||
uint32_t rx_frames = 0;
|
||||
uint32_t tx_frames = 0;
|
||||
size_t last_len = 0;
|
||||
int params_count;
|
||||
cJSON *last_item;
|
||||
cJSON *idx_item;
|
||||
int idx;
|
||||
|
||||
ESP_LOGI(TAG, "n_signer v%s booting...", FIRMWARE_VERSION);
|
||||
printf("n_signer booting\n");
|
||||
|
||||
ESP_ERROR_CHECK(display_init());
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 10, "n_signer", RGB565_YELLOW, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 30, "deriving keys...", RGB565_YELLOW, RGB565_BLACK));
|
||||
|
||||
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
||||
memset(s_seed, 0, sizeof(s_seed));
|
||||
memset(s_privkey, 0, sizeof(s_privkey));
|
||||
memset(s_pubkey, 0, sizeof(s_pubkey));
|
||||
memset(s_npub, 0, sizeof(s_npub));
|
||||
memset(s_npub_short, 0, sizeof(s_npub_short));
|
||||
memset(s_mnemonic_short, 0, sizeof(s_mnemonic_short));
|
||||
memset(s_frame_buf, 0, sizeof(s_frame_buf));
|
||||
memset(s_pubkey_hex, 0, sizeof(s_pubkey_hex));
|
||||
|
||||
if (generate_mnemonic_12(s_mnemonic, sizeof(s_mnemonic)) != 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 50, "mnemonic failed", RGB565_RED, RGB565_BLACK));
|
||||
return;
|
||||
if (out_index == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("BIP39 mnemonic: %s\n", s_mnemonic);
|
||||
format_mnemonic_short(s_mnemonic, s_mnemonic_short, sizeof(s_mnemonic_short));
|
||||
*out_index = 0;
|
||||
|
||||
if (mnemonic_to_seed(s_mnemonic, s_seed) != 0) {
|
||||
if (params == NULL || !cJSON_IsArray(params)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
params_count = cJSON_GetArraySize(params);
|
||||
if (params_count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
last_item = cJSON_GetArrayItem(params, params_count - 1);
|
||||
if (last_item == NULL || !cJSON_IsObject(last_item)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
idx_item = cJSON_GetObjectItemCaseSensitive(last_item, "nostr_index");
|
||||
if (idx_item == NULL) {
|
||||
return 0;
|
||||
}
|
||||
if (!cJSON_IsNumber(idx_item)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = idx_item->valueint;
|
||||
if (idx < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_index = (uint32_t)idx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int derive_request_key(uint32_t nostr_index, uint8_t privkey_out[32], char pubkey_hex_out[65])
|
||||
{
|
||||
uint8_t pubkey[32];
|
||||
|
||||
if (privkey_out == NULL || pubkey_hex_out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (derive_nostr_key_index(s_seed, nostr_index, privkey_out, pubkey) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bytes_to_hex(pubkey, sizeof(pubkey), pubkey_hex_out, 65);
|
||||
secure_memzero(pubkey, sizeof(pubkey));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_peer_and_message_params(cJSON *params,
|
||||
const char **peer_hex_out,
|
||||
const char **message_out,
|
||||
uint32_t *nostr_index_out)
|
||||
{
|
||||
cJSON *peer_item;
|
||||
cJSON *msg_item;
|
||||
|
||||
if (params == NULL || !cJSON_IsArray(params) ||
|
||||
peer_hex_out == NULL || message_out == NULL || nostr_index_out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (cJSON_GetArraySize(params) < 2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
peer_item = cJSON_GetArrayItem(params, 0);
|
||||
msg_item = cJSON_GetArrayItem(params, 1);
|
||||
|
||||
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
|
||||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (parse_nostr_index_from_params(params, nostr_index_out) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*peer_hex_out = peer_item->valuestring;
|
||||
*message_out = msg_item->valuestring;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_mnemonic_and_enter_working(const char *mnemonic)
|
||||
{
|
||||
if (mnemonic == NULL || mnemonic[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
format_mnemonic_short(mnemonic, s_mnemonic_short, sizeof(s_mnemonic_short));
|
||||
|
||||
if (mnemonic_to_seed(mnemonic, s_seed) != 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 50, "seed failed", RGB565_RED, RGB565_BLACK));
|
||||
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (derive_nostr_key(s_seed, s_privkey, s_pubkey) != 0) {
|
||||
if (derive_nostr_key_index(s_seed, 0u, s_privkey, s_pubkey) != 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 50, "key failed", RGB565_RED, RGB565_BLACK));
|
||||
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
||||
memset(s_seed, 0, sizeof(s_seed));
|
||||
return;
|
||||
secure_memzero(s_seed, sizeof(s_seed));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (npub_encode(s_pubkey, s_npub, sizeof(s_npub)) != 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 50, "npub failed", RGB565_RED, RGB565_BLACK));
|
||||
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
||||
memset(s_seed, 0, sizeof(s_seed));
|
||||
memset(s_privkey, 0, sizeof(s_privkey));
|
||||
memset(s_pubkey, 0, sizeof(s_pubkey));
|
||||
return;
|
||||
secure_memzero(s_seed, sizeof(s_seed));
|
||||
secure_memzero(s_pubkey, sizeof(s_pubkey));
|
||||
return -1;
|
||||
}
|
||||
|
||||
format_npub_short(s_npub, s_npub_short, sizeof(s_npub_short));
|
||||
bytes_to_hex(s_pubkey, sizeof(s_pubkey), s_pubkey_hex, sizeof(s_pubkey_hex));
|
||||
printf("Derived npub: %s\n", s_npub);
|
||||
printf("Derived pubkey hex: %s\n", s_pubkey_hex);
|
||||
printf("Derived npub[0]: %s\n", s_npub);
|
||||
printf("Derived pubkey hex[0]: %s\n", s_pubkey_hex);
|
||||
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 10, "nostr npub", RGB565_GREEN, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 30, s_npub_short, RGB565_GREEN, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 50, "mn:", RGB565_WHITE, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(46, 50, s_mnemonic_short, RGB565_YELLOW, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 70, "v" FIRMWARE_VERSION, RGB565_WHITE, RGB565_BLACK));
|
||||
show_idle_screen();
|
||||
|
||||
memset(s_seed, 0, sizeof(s_seed));
|
||||
memset(s_pubkey, 0, sizeof(s_pubkey));
|
||||
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
||||
secure_memzero(s_pubkey, sizeof(s_pubkey));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 90, "usb cdc+webusb", RGB565_CYAN, RGB565_BLACK));
|
||||
printf("usb cdc+webusb ready\n");
|
||||
|
||||
/* Prevent log text from corrupting framed transport bytes on this same USB stream. */
|
||||
esp_log_level_set("*", ESP_LOG_NONE);
|
||||
|
||||
buttons_init();
|
||||
|
||||
if (transport_init() != 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 90, "cdc init fail", RGB565_RED, RGB565_BLACK));
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/* WebUSB temporarily disabled to preserve stable USB Serial/JTAG CDC enumeration. */
|
||||
static void run_signing_loop(void)
|
||||
{
|
||||
uint32_t rx_frames = 0;
|
||||
uint32_t tx_frames = 0;
|
||||
size_t last_len = 0;
|
||||
|
||||
while (1) {
|
||||
char stat1[32];
|
||||
@@ -870,6 +858,12 @@ void app_main(void)
|
||||
|
||||
rx_frames++;
|
||||
|
||||
/* Default to a concrete error so every received frame gets a response. */
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32603,\"message\":\"internal error: unhandled request\"}}");
|
||||
|
||||
if (last_len < sizeof(s_frame_buf)) {
|
||||
char id_token[96] = {0};
|
||||
char method[64] = {0};
|
||||
@@ -914,37 +908,56 @@ void app_main(void)
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}",
|
||||
id_token, auth_rc, msg);
|
||||
} else if (strcmp(method, "get_public_key") == 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
|
||||
id_token,
|
||||
s_pubkey_hex);
|
||||
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
||||
uint32_t nostr_index = 0;
|
||||
uint8_t req_privkey[32];
|
||||
char req_pubkey_hex[65];
|
||||
|
||||
memset(req_privkey, 0, sizeof(req_privkey));
|
||||
memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
|
||||
|
||||
if (!cJSON_IsArray(params) ||
|
||||
parse_nostr_index_from_params(params, &nostr_index) != 0 ||
|
||||
derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
|
||||
id_token,
|
||||
req_pubkey_hex);
|
||||
}
|
||||
|
||||
secure_memzero(req_privkey, sizeof(req_privkey));
|
||||
} else if (strcmp(method, "sign_event") == 0) {
|
||||
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
||||
cJSON *event_in = NULL;
|
||||
uint32_t nostr_index = 0;
|
||||
uint8_t req_privkey[32];
|
||||
char req_pubkey_hex[65];
|
||||
|
||||
memset(req_privkey, 0, sizeof(req_privkey));
|
||||
memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
|
||||
|
||||
if (cJSON_IsArray(params) && cJSON_GetArraySize(params) > 0) {
|
||||
event_in = cJSON_GetArrayItem(params, 0);
|
||||
}
|
||||
|
||||
if (event_in != NULL && cJSON_IsObject(event_in)) {
|
||||
approval_decision_t decision;
|
||||
if (event_in != NULL && cJSON_IsObject(event_in) &&
|
||||
parse_nostr_index_from_params(params, &nostr_index) == 0 &&
|
||||
derive_request_key(nostr_index, req_privkey, req_pubkey_hex) == 0) {
|
||||
approval_decision_t decision = APPROVAL_ALWAYS;
|
||||
|
||||
if (s_always_allow_sign) {
|
||||
decision = APPROVAL_ALWAYS;
|
||||
} else {
|
||||
show_approval_prompt_with_caller(event_in, caller_pubkey_hex);
|
||||
decision = wait_for_approval(APPROVAL_TIMEOUT_MS);
|
||||
show_idle_screen();
|
||||
}
|
||||
|
||||
if (decision == APPROVAL_ALWAYS) {
|
||||
s_always_allow_sign = true;
|
||||
}
|
||||
/* Firmware policy: never prompt, always approve sign requests. */
|
||||
s_always_allow_sign = true;
|
||||
|
||||
if ((decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) &&
|
||||
build_signed_event_json(event_in, s_pubkey_hex, s_privkey, s_signed_event_buf, sizeof(s_signed_event_buf)) == 0) {
|
||||
build_signed_event_json(event_in, req_pubkey_hex, req_privkey, s_signed_event_buf, sizeof(s_signed_event_buf)) == 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
@@ -977,6 +990,212 @@ void app_main(void)
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
||||
id_token);
|
||||
}
|
||||
|
||||
secure_memzero(req_privkey, sizeof(req_privkey));
|
||||
} else if (strcmp(method, "nip04_encrypt") == 0) {
|
||||
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
||||
const char *peer_hex = NULL;
|
||||
const char *plaintext = NULL;
|
||||
uint32_t nostr_index = 0;
|
||||
uint8_t peer_pubkey[32];
|
||||
uint8_t req_privkey[32];
|
||||
char req_pubkey_hex[65];
|
||||
|
||||
memset(peer_pubkey, 0, sizeof(peer_pubkey));
|
||||
memset(req_privkey, 0, sizeof(req_privkey));
|
||||
memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
|
||||
memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));
|
||||
|
||||
if (parse_peer_and_message_params(params, &peer_hex, &plaintext, &nostr_index) != 0 ||
|
||||
hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
|
||||
derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
||||
id_token);
|
||||
} else if (nostr_nip04_encrypt(req_privkey, peer_pubkey, plaintext,
|
||||
s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip04 encryption failed\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
|
||||
id_token,
|
||||
s_encrypt_buf);
|
||||
}
|
||||
|
||||
secure_memzero(req_privkey, sizeof(req_privkey));
|
||||
secure_memzero(peer_pubkey, sizeof(peer_pubkey));
|
||||
} else if (strcmp(method, "nip44_encrypt") == 0) {
|
||||
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
||||
const char *peer_hex = NULL;
|
||||
const char *plaintext = NULL;
|
||||
uint32_t nostr_index = 0;
|
||||
uint8_t peer_pubkey[32];
|
||||
uint8_t req_privkey[32];
|
||||
char req_pubkey_hex[65];
|
||||
|
||||
memset(peer_pubkey, 0, sizeof(peer_pubkey));
|
||||
memset(req_privkey, 0, sizeof(req_privkey));
|
||||
memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
|
||||
memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));
|
||||
|
||||
if (parse_peer_and_message_params(params, &peer_hex, &plaintext, &nostr_index) != 0 ||
|
||||
hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
|
||||
derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
||||
id_token);
|
||||
} else if (nostr_nip44_encrypt(req_privkey, peer_pubkey, plaintext,
|
||||
s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip44 encryption failed\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
|
||||
id_token,
|
||||
s_encrypt_buf);
|
||||
}
|
||||
|
||||
secure_memzero(req_privkey, sizeof(req_privkey));
|
||||
secure_memzero(peer_pubkey, sizeof(peer_pubkey));
|
||||
} else if (strcmp(method, "nip04_decrypt") == 0) {
|
||||
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
||||
const char *peer_hex = NULL;
|
||||
const char *ciphertext = NULL;
|
||||
uint32_t nostr_index = 0;
|
||||
uint8_t peer_pubkey[32];
|
||||
uint8_t req_privkey[32];
|
||||
char req_pubkey_hex[65];
|
||||
|
||||
memset(peer_pubkey, 0, sizeof(peer_pubkey));
|
||||
memset(req_privkey, 0, sizeof(req_privkey));
|
||||
memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
|
||||
memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));
|
||||
|
||||
if (parse_peer_and_message_params(params, &peer_hex, &ciphertext, &nostr_index) != 0 ||
|
||||
hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
|
||||
derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
||||
id_token);
|
||||
} else if (nostr_nip04_decrypt(req_privkey, peer_pubkey, ciphertext,
|
||||
s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip04 decryption failed\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
cJSON *resp_obj = cJSON_CreateObject();
|
||||
if (resp_obj == NULL) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
cJSON_AddStringToObject(resp_obj, "jsonrpc", "2.0");
|
||||
cJSON_AddRawToObject(resp_obj, "id", id_token);
|
||||
cJSON_AddStringToObject(resp_obj, "result", s_encrypt_buf);
|
||||
{
|
||||
char *resp_json = cJSON_PrintUnformatted(resp_obj);
|
||||
if (resp_json != NULL && strlen(resp_json) < sizeof(s_response_buf)) {
|
||||
memcpy(s_response_buf, resp_json, strlen(resp_json) + 1);
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
|
||||
id_token);
|
||||
}
|
||||
if (resp_json != NULL) {
|
||||
cJSON_free(resp_json);
|
||||
}
|
||||
}
|
||||
cJSON_Delete(resp_obj);
|
||||
}
|
||||
}
|
||||
|
||||
secure_memzero(req_privkey, sizeof(req_privkey));
|
||||
secure_memzero(peer_pubkey, sizeof(peer_pubkey));
|
||||
} else if (strcmp(method, "nip44_decrypt") == 0) {
|
||||
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
||||
const char *peer_hex = NULL;
|
||||
const char *ciphertext = NULL;
|
||||
uint32_t nostr_index = 0;
|
||||
uint8_t peer_pubkey[32];
|
||||
uint8_t req_privkey[32];
|
||||
char req_pubkey_hex[65];
|
||||
|
||||
memset(peer_pubkey, 0, sizeof(peer_pubkey));
|
||||
memset(req_privkey, 0, sizeof(req_privkey));
|
||||
memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex));
|
||||
memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf));
|
||||
|
||||
if (parse_peer_and_message_params(params, &peer_hex, &ciphertext, &nostr_index) != 0 ||
|
||||
hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 ||
|
||||
derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
||||
id_token);
|
||||
} else if (nostr_nip44_decrypt(req_privkey, peer_pubkey, ciphertext,
|
||||
s_encrypt_buf, sizeof(s_encrypt_buf)) != NOSTR_SUCCESS) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"nip44 decryption failed\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
cJSON *resp_obj = cJSON_CreateObject();
|
||||
if (resp_obj == NULL) {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
|
||||
id_token);
|
||||
} else {
|
||||
cJSON_AddStringToObject(resp_obj, "jsonrpc", "2.0");
|
||||
cJSON_AddRawToObject(resp_obj, "id", id_token);
|
||||
cJSON_AddStringToObject(resp_obj, "result", s_encrypt_buf);
|
||||
{
|
||||
char *resp_json = cJSON_PrintUnformatted(resp_obj);
|
||||
if (resp_json != NULL && strlen(resp_json) < sizeof(s_response_buf)) {
|
||||
memcpy(s_response_buf, resp_json, strlen(resp_json) + 1);
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
|
||||
id_token);
|
||||
}
|
||||
if (resp_json != NULL) {
|
||||
cJSON_free(resp_json);
|
||||
}
|
||||
}
|
||||
cJSON_Delete(resp_obj);
|
||||
}
|
||||
}
|
||||
|
||||
secure_memzero(req_privkey, sizeof(req_privkey));
|
||||
secure_memzero(peer_pubkey, sizeof(peer_pubkey));
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
@@ -993,13 +1212,82 @@ void app_main(void)
|
||||
if ((!via_webusb && send_frame_stdout((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0) ||
|
||||
(via_webusb && webusb_send_frame((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0)) {
|
||||
tx_frames++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "failed to send JSON-RPC response over %s", via_webusb ? "WebUSB" : "CDC");
|
||||
}
|
||||
} else {
|
||||
snprintf(
|
||||
s_response_buf,
|
||||
sizeof(s_response_buf),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"request too large\"}}");
|
||||
|
||||
if ((!via_webusb && send_frame_stdout((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0) ||
|
||||
(via_webusb && webusb_send_frame((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0)) {
|
||||
tx_frames++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "failed to send oversized-request error over %s", via_webusb ? "WebUSB" : "CDC");
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(stat1, sizeof(stat1), "rx:%" PRIu32 " tx:%" PRIu32, rx_frames, tx_frames);
|
||||
snprintf(stat2, sizeof(stat2), "last:%uB", (unsigned)last_len);
|
||||
ESP_ERROR_CHECK(display_fill_rect(10, 110, 220, 24, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 110, stat1, RGB565_WHITE, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 122, stat2, RGB565_CYAN, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 110, stat1, RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 122, stat2, RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "n_signer v%s booting...", FIRMWARE_VERSION);
|
||||
printf("n_signer booting\n");
|
||||
|
||||
ESP_ERROR_CHECK(display_init());
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
|
||||
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
||||
memset(s_seed, 0, sizeof(s_seed));
|
||||
memset(s_privkey, 0, sizeof(s_privkey));
|
||||
memset(s_pubkey, 0, sizeof(s_pubkey));
|
||||
memset(s_npub, 0, sizeof(s_npub));
|
||||
memset(s_npub_short, 0, sizeof(s_npub_short));
|
||||
memset(s_mnemonic_short, 0, sizeof(s_mnemonic_short));
|
||||
memset(s_frame_buf, 0, sizeof(s_frame_buf));
|
||||
memset(s_pubkey_hex, 0, sizeof(s_pubkey_hex));
|
||||
|
||||
if (ui_run_until_working(s_mnemonic) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "UI FAILED", RGB565_RED, RGB565_BLACK));
|
||||
return;
|
||||
}
|
||||
|
||||
if (apply_mnemonic_and_enter_working(s_mnemonic) != 0) {
|
||||
secure_memzero(s_mnemonic, sizeof(s_mnemonic));
|
||||
return;
|
||||
}
|
||||
|
||||
secure_memzero(s_mnemonic, sizeof(s_mnemonic));
|
||||
printf("usb cdc+webusb ready\n");
|
||||
|
||||
/* Prevent log text from corrupting framed transport bytes on this same USB stream. */
|
||||
esp_log_level_set("*", ESP_LOG_NONE);
|
||||
|
||||
buttons_init();
|
||||
|
||||
if (transport_init() != 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 90, "cdc init fail", RGB565_RED, RGB565_BLACK));
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 90, "nostr init fail", RGB565_RED, RGB565_BLACK));
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/* WebUSB temporarily disabled to preserve stable USB Serial/JTAG CDC enumeration. */
|
||||
run_signing_loop();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
|
||||
@@ -18,6 +19,10 @@
|
||||
#define CHECKSUM_12_BITS 4
|
||||
#define TOTAL_12_BITS (ENTROPY_12_BYTES * 8 + CHECKSUM_12_BITS)
|
||||
|
||||
static bool s_letter_index_built = false;
|
||||
static size_t s_letter_start[26] = {0};
|
||||
static size_t s_letter_count[26] = {0};
|
||||
|
||||
static bool append_word(char *out, size_t out_len, size_t *cursor, const char *word)
|
||||
{
|
||||
const size_t word_len = strlen(word);
|
||||
@@ -59,6 +64,74 @@ static uint8_t get_bit_from_entropy_checksum(const uint8_t entropy[ENTROPY_12_BY
|
||||
return (uint8_t)((checksum[0] >> bit_in_byte) & 0x01);
|
||||
}
|
||||
|
||||
static int bip39_word_index(const char *word)
|
||||
{
|
||||
if (word == NULL || word[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
if (strcmp(word, BIP39_WORDLIST[i]) == 0) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool next_token(const char **cursor, char *out_word, size_t out_len)
|
||||
{
|
||||
size_t n = 0;
|
||||
const char *p = *cursor;
|
||||
|
||||
while (*p != '\0' && isspace((unsigned char)*p)) {
|
||||
++p;
|
||||
}
|
||||
|
||||
if (*p == '\0') {
|
||||
*cursor = p;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*p != '\0' && !isspace((unsigned char)*p)) {
|
||||
if (n + 1 >= out_len) {
|
||||
return false;
|
||||
}
|
||||
out_word[n++] = *p++;
|
||||
}
|
||||
|
||||
out_word[n] = '\0';
|
||||
*cursor = p;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void build_letter_index(void)
|
||||
{
|
||||
if (s_letter_index_built) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 26; ++i) {
|
||||
s_letter_start[i] = 0;
|
||||
s_letter_count[i] = 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
char c = BIP39_WORDLIST[i][0];
|
||||
if (c < 'a' || c > 'z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t li = (size_t)(c - 'a');
|
||||
if (s_letter_count[li] == 0) {
|
||||
s_letter_start[li] = i;
|
||||
}
|
||||
s_letter_count[li]++;
|
||||
}
|
||||
|
||||
s_letter_index_built = true;
|
||||
}
|
||||
|
||||
int generate_mnemonic_12(char *out, size_t out_len)
|
||||
{
|
||||
uint8_t entropy[ENTROPY_12_BYTES] = {0};
|
||||
@@ -109,6 +182,109 @@ int generate_mnemonic_12(char *out, size_t out_len)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mnemonic_validate(const char *mnemonic)
|
||||
{
|
||||
uint16_t word_indices[MNEMONIC_12_WORDS] = {0};
|
||||
uint8_t entropy[ENTROPY_12_BYTES] = {0};
|
||||
uint8_t hash[32] = {0};
|
||||
const char *cursor = mnemonic;
|
||||
char token[16];
|
||||
size_t words = 0;
|
||||
size_t bit_pos = 0;
|
||||
|
||||
if (mnemonic == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (next_token(&cursor, token, sizeof(token))) {
|
||||
int idx;
|
||||
|
||||
if (words >= MNEMONIC_12_WORDS) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = bip39_word_index(token);
|
||||
if (idx < 0) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
word_indices[words++] = (uint16_t)idx;
|
||||
}
|
||||
|
||||
if (words != MNEMONIC_12_WORDS) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t w = 0; w < MNEMONIC_12_WORDS; ++w) {
|
||||
for (int b = 10; b >= 0; --b) {
|
||||
uint8_t bit = (uint8_t)((word_indices[w] >> b) & 0x01);
|
||||
if (bit_pos < ENTROPY_12_BYTES * 8) {
|
||||
size_t byte_index = bit_pos / 8;
|
||||
size_t bit_in_byte = 7 - (bit_pos % 8);
|
||||
entropy[byte_index] |= (uint8_t)(bit << bit_in_byte);
|
||||
}
|
||||
bit_pos++;
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_sha256(entropy, sizeof(entropy), hash, 0);
|
||||
|
||||
for (size_t i = 0; i < CHECKSUM_12_BITS; ++i) {
|
||||
size_t checksum_bit_pos = ENTROPY_12_BYTES * 8 + i;
|
||||
size_t word_idx = checksum_bit_pos / 11;
|
||||
size_t bit_in_word = 10 - (checksum_bit_pos % 11);
|
||||
uint8_t expected = (uint8_t)((hash[0] >> (7 - i)) & 0x01);
|
||||
uint8_t actual = (uint8_t)((word_indices[word_idx] >> bit_in_word) & 0x01);
|
||||
if (actual != expected) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t mnemonic_words_for_letter(char letter, size_t *out_start)
|
||||
{
|
||||
int idx;
|
||||
|
||||
if (out_start == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (letter >= 'A' && letter <= 'Z') {
|
||||
letter = (char)(letter - 'A' + 'a');
|
||||
}
|
||||
|
||||
if (letter < 'a' || letter > 'z') {
|
||||
*out_start = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
build_letter_index();
|
||||
|
||||
idx = letter - 'a';
|
||||
*out_start = s_letter_start[idx];
|
||||
return s_letter_count[idx];
|
||||
}
|
||||
|
||||
const char *mnemonic_word_at(size_t index)
|
||||
{
|
||||
if (index >= 2048) {
|
||||
return NULL;
|
||||
}
|
||||
return BIP39_WORDLIST[index];
|
||||
}
|
||||
|
||||
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64])
|
||||
{
|
||||
const mbedtls_md_info_t *md_info;
|
||||
|
||||
@@ -5,3 +5,6 @@
|
||||
|
||||
int generate_mnemonic_12(char *out, size_t out_len);
|
||||
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64]);
|
||||
int mnemonic_validate(const char *mnemonic);
|
||||
size_t mnemonic_words_for_letter(char letter, size_t *out_start);
|
||||
const char *mnemonic_word_at(size_t index);
|
||||
|
||||
15
firmware/feather_s3_tft/main/secure_mem.c
Normal file
15
firmware/feather_s3_tft/main/secure_mem.c
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "secure_mem.h"
|
||||
|
||||
void secure_memzero(void *ptr, size_t len)
|
||||
{
|
||||
volatile unsigned char *p = (volatile unsigned char *)ptr;
|
||||
|
||||
if (p == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
*p++ = 0;
|
||||
len--;
|
||||
}
|
||||
}
|
||||
13
firmware/feather_s3_tft/main/secure_mem.h
Normal file
13
firmware/feather_s3_tft/main/secure_mem.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void secure_memzero(void *ptr, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
680
firmware/feather_s3_tft/main/ui.c
Normal file
680
firmware/feather_s3_tft/main/ui.c
Normal file
@@ -0,0 +1,680 @@
|
||||
#include "ui.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "buttons.h"
|
||||
#include "display.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "mnemonic.h"
|
||||
#include "secure_mem.h"
|
||||
|
||||
typedef enum {
|
||||
UI_MAIN_GENERATE = 0,
|
||||
UI_MAIN_ENTER,
|
||||
UI_MAIN_VIEW,
|
||||
} ui_main_menu_item_t;
|
||||
|
||||
#define UI_CHAR_W_SMALL 6
|
||||
#define UI_CHAR_W_LARGE 12
|
||||
|
||||
typedef struct {
|
||||
size_t count;
|
||||
size_t first_idx[3];
|
||||
int exact_idx;
|
||||
} prefix_matches_t;
|
||||
|
||||
typedef struct {
|
||||
char phrase[256];
|
||||
bool has_phrase;
|
||||
|
||||
ui_main_menu_item_t main_sel;
|
||||
|
||||
int letter_idx; /* 0..25 */
|
||||
char prefix[12];
|
||||
size_t prefix_len;
|
||||
|
||||
char entered_words[12][12];
|
||||
size_t entered_count;
|
||||
} ui_state_t;
|
||||
|
||||
static btn_event_t wait_any_button(void)
|
||||
{
|
||||
btn_event_t evt;
|
||||
do {
|
||||
evt = buttons_wait(0xFFFFFFFFu);
|
||||
} while (evt.kind == BTN_EVT_NONE);
|
||||
return evt;
|
||||
}
|
||||
|
||||
static void draw_splash(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(60, "N_SIGNER", RGB565_RED, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_main_menu_row(bool has_phrase, int row_idx, bool selected)
|
||||
{
|
||||
static const char *items[3] = {"Generate", "Enter", "View"};
|
||||
static const int ys[3] = {28, 52, 76};
|
||||
bool disabled = (row_idx == UI_MAIN_VIEW && !has_phrase);
|
||||
char line[24];
|
||||
|
||||
snprintf(line, sizeof(line), "%s%s", selected ? "> " : " ", items[row_idx]);
|
||||
ESP_ERROR_CHECK(display_fill_rect(24, ys[row_idx], 200, 16, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(
|
||||
24,
|
||||
ys[row_idx],
|
||||
line,
|
||||
disabled ? RGB565_DIM_GRAY : (selected ? RGB565_RED : RGB565_WHITE),
|
||||
RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_main_menu_full(const ui_state_t *st)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(4, "MAIN MENU", RGB565_WHITE, RGB565_BLACK));
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
draw_main_menu_row(st->has_phrase, i, ((int)st->main_sel == i));
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text_small(12, 120, "D2 up D1 dn D0 ok", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_main_menu_selection_change(bool has_phrase, int prev_sel, int new_sel)
|
||||
{
|
||||
draw_main_menu_row(has_phrase, prev_sel, false);
|
||||
draw_main_menu_row(has_phrase, new_sel, true);
|
||||
}
|
||||
|
||||
static void split_phrase_words(const char *phrase, char words[12][12])
|
||||
{
|
||||
char buf[256];
|
||||
char *tok;
|
||||
size_t i = 0;
|
||||
|
||||
memset(words, 0, 12 * 12);
|
||||
|
||||
if (phrase == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(buf, phrase, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
|
||||
tok = strtok(buf, " \t\r\n");
|
||||
while (tok != NULL && i < 12) {
|
||||
strncpy(words[i], tok, 11);
|
||||
words[i][11] = '\0';
|
||||
i++;
|
||||
tok = strtok(NULL, " \t\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_word_grid(const char words[12][12], int highlight_idx)
|
||||
{
|
||||
const int col_w = 80;
|
||||
const int row_h = 24;
|
||||
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
int row = i / 3;
|
||||
int col = i % 3;
|
||||
int x = col * col_w + 2;
|
||||
int y = 14 + row * row_h;
|
||||
char line[20];
|
||||
|
||||
snprintf(line, sizeof(line), "%2d. %.11s", i + 1, words[i]);
|
||||
ESP_ERROR_CHECK(display_draw_text_small(
|
||||
x,
|
||||
y,
|
||||
line,
|
||||
(i == highlight_idx) ? RGB565_RED : RGB565_WHITE,
|
||||
RGB565_BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_view_page(const ui_state_t *st)
|
||||
{
|
||||
char words[12][12];
|
||||
|
||||
split_phrase_words(st->phrase, words);
|
||||
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_small_centered(2, "YOUR MNEMONIC", RGB565_WHITE, RGB565_BLACK));
|
||||
draw_word_grid(words, -1);
|
||||
ESP_ERROR_CHECK(display_draw_text_small_centered(120, "any key = back", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_generate_page(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "GENERATING...", RGB565_WHITE, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void clear_enter_words(ui_state_t *st)
|
||||
{
|
||||
secure_memzero(st->entered_words, sizeof(st->entered_words));
|
||||
st->entered_count = 0;
|
||||
st->prefix_len = 0;
|
||||
st->prefix[0] = '\0';
|
||||
}
|
||||
|
||||
static void letter_cell_pos(int idx, int *x, int *y)
|
||||
{
|
||||
int row = (idx < 13) ? 0 : 1;
|
||||
int col = (idx < 13) ? idx : (idx - 13);
|
||||
*x = 10 + col * 16;
|
||||
*y = row ? 68 : 44;
|
||||
}
|
||||
|
||||
static void compute_allowed_next_letters(const ui_state_t *st, bool allowed[26])
|
||||
{
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
allowed[i] = false;
|
||||
}
|
||||
|
||||
if (st->prefix_len == 0) {
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
allowed[i] = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
const char *w = mnemonic_word_at(i);
|
||||
size_t wlen;
|
||||
char c;
|
||||
int idx;
|
||||
|
||||
if (w == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (strncmp(w, st->prefix, st->prefix_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wlen = strlen(w);
|
||||
if (wlen <= st->prefix_len) {
|
||||
continue;
|
||||
}
|
||||
|
||||
c = w[st->prefix_len];
|
||||
if (c < 'a' || c > 'z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
idx = (int)(c - 'a');
|
||||
allowed[idx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
static int find_next_allowed_letter(int current, int delta, const bool allowed[26])
|
||||
{
|
||||
int idx = current;
|
||||
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
idx = (idx + delta + 26) % 26;
|
||||
if (allowed[idx]) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
static int first_allowed_letter(const bool allowed[26])
|
||||
{
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
if (allowed[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void draw_enter_letter_cell(int idx, bool selected, const bool allowed[26])
|
||||
{
|
||||
int x;
|
||||
int y;
|
||||
char c[2] = {(char)('A' + idx), '\0'};
|
||||
uint16_t color = RGB565_WHITE;
|
||||
|
||||
letter_cell_pos(idx, &x, &y);
|
||||
ESP_ERROR_CHECK(display_fill_rect(x, y, 12, 16, RGB565_BLACK));
|
||||
|
||||
if (!allowed[idx]) {
|
||||
color = RGB565_DIM_GRAY;
|
||||
}
|
||||
if (selected && allowed[idx]) {
|
||||
color = RGB565_RED;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text(x, y, c, color, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_enter_letter_cursor_change(int prev_idx, int new_idx, const bool allowed[26])
|
||||
{
|
||||
draw_enter_letter_cell(prev_idx, false, allowed);
|
||||
draw_enter_letter_cell(new_idx, true, allowed);
|
||||
}
|
||||
|
||||
static void compute_prefix_matches(const ui_state_t *st, prefix_matches_t *m)
|
||||
{
|
||||
m->count = 0;
|
||||
m->first_idx[0] = 0;
|
||||
m->first_idx[1] = 0;
|
||||
m->first_idx[2] = 0;
|
||||
m->exact_idx = -1;
|
||||
|
||||
if (st->prefix_len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
const char *w = mnemonic_word_at(i);
|
||||
if (w == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncmp(w, st->prefix, st->prefix_len) == 0) {
|
||||
if (m->count < 3) {
|
||||
m->first_idx[m->count] = i;
|
||||
}
|
||||
m->count++;
|
||||
if (strcmp(w, st->prefix) == 0 && m->exact_idx < 0) {
|
||||
m->exact_idx = (int)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_enter_compact_static(const ui_state_t *st, const bool allowed[26])
|
||||
{
|
||||
char hdr[24];
|
||||
unsigned current_word = (unsigned)(st->entered_count + 1);
|
||||
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
|
||||
if (current_word > 12) {
|
||||
current_word = 12;
|
||||
}
|
||||
snprintf(hdr, sizeof(hdr), "WORD %u/12", current_word);
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 8, hdr, RGB565_WHITE, RGB565_BLACK));
|
||||
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
draw_enter_letter_cell(i, i == st->letter_idx, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
static int draw_word_with_prefix_large(int x, int y, const char *word, size_t prefix_len)
|
||||
{
|
||||
char pref[12];
|
||||
char rest[12];
|
||||
size_t wlen;
|
||||
size_t plen;
|
||||
|
||||
if (word == NULL) {
|
||||
return x;
|
||||
}
|
||||
|
||||
wlen = strlen(word);
|
||||
plen = (prefix_len > wlen) ? wlen : prefix_len;
|
||||
|
||||
memset(pref, 0, sizeof(pref));
|
||||
memset(rest, 0, sizeof(rest));
|
||||
|
||||
if (plen > 0) {
|
||||
memcpy(pref, word, plen);
|
||||
}
|
||||
if (wlen > plen) {
|
||||
memcpy(rest, word + plen, wlen - plen);
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text(x, y, pref, RGB565_RED, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(x + ((int)plen * UI_CHAR_W_LARGE), y, rest, RGB565_WHITE, RGB565_BLACK));
|
||||
|
||||
return x + ((int)wlen * UI_CHAR_W_LARGE);
|
||||
}
|
||||
|
||||
static void draw_match_row_large(const ui_state_t *st)
|
||||
{
|
||||
int x = 4;
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
const char *w = mnemonic_word_at(i);
|
||||
size_t wlen;
|
||||
int needed;
|
||||
|
||||
if (w == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (strncmp(w, st->prefix, st->prefix_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wlen = strlen(w);
|
||||
needed = (int)(wlen * UI_CHAR_W_LARGE) + UI_CHAR_W_LARGE;
|
||||
if (x + needed > DISPLAY_WIDTH) {
|
||||
break;
|
||||
}
|
||||
|
||||
x = draw_word_with_prefix_large(x, 88, w, st->prefix_len);
|
||||
ESP_ERROR_CHECK(display_draw_text(x, 88, " ", RGB565_WHITE, RGB565_BLACK));
|
||||
x += UI_CHAR_W_LARGE;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_enter_compact_dynamic(const ui_state_t *st, const prefix_matches_t *m)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_fill_rect(0, 86, DISPLAY_WIDTH, 49, RGB565_BLACK));
|
||||
|
||||
if (st->prefix_len == 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 118, "D0=letter D1h=back D2h=del", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m->count == 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(88, "NO MATCH", RGB565_RED, RGB565_BLACK));
|
||||
} else {
|
||||
draw_match_row_large(st);
|
||||
}
|
||||
|
||||
if (m->count == 1) {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 108, "One word left: D0=YES D2=NO", RGB565_RED, RGB565_BLACK));
|
||||
}
|
||||
|
||||
if (st->entered_count < 12) {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 118, "D0=letter D1h=back D2h=del", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
} else {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 118, "12/12: D0 hold=save D2 hold=del", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
static int build_phrase_from_entered(ui_state_t *st)
|
||||
{
|
||||
size_t cursor = 0;
|
||||
|
||||
st->phrase[0] = '\0';
|
||||
for (size_t i = 0; i < st->entered_count; ++i) {
|
||||
size_t wlen = strlen(st->entered_words[i]);
|
||||
|
||||
if (i > 0) {
|
||||
if (cursor + 1 >= sizeof(st->phrase)) {
|
||||
return -1;
|
||||
}
|
||||
st->phrase[cursor++] = ' ';
|
||||
}
|
||||
|
||||
if (cursor + wlen >= sizeof(st->phrase)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(st->phrase + cursor, st->entered_words[i], wlen);
|
||||
cursor += wlen;
|
||||
st->phrase[cursor] = '\0';
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ui_run_until_working(char out_mnemonic[256])
|
||||
{
|
||||
ui_state_t st;
|
||||
bool main_menu_drawn = false;
|
||||
|
||||
if (out_mnemonic == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.main_sel = UI_MAIN_GENERATE;
|
||||
|
||||
draw_splash();
|
||||
(void)wait_any_button();
|
||||
|
||||
while (1) {
|
||||
btn_event_t evt;
|
||||
|
||||
if (!main_menu_drawn) {
|
||||
draw_main_menu_full(&st);
|
||||
main_menu_drawn = true;
|
||||
}
|
||||
|
||||
evt = wait_any_button();
|
||||
|
||||
if (evt.id == BTN_D2) {
|
||||
int prev = (int)st.main_sel;
|
||||
if (st.has_phrase) {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_GENERATE) ? UI_MAIN_VIEW : (ui_main_menu_item_t)((int)st.main_sel - 1);
|
||||
} else {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_GENERATE) ? UI_MAIN_ENTER : UI_MAIN_GENERATE;
|
||||
}
|
||||
if ((int)st.main_sel != prev) {
|
||||
draw_main_menu_selection_change(st.has_phrase, prev, (int)st.main_sel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D1) {
|
||||
int prev = (int)st.main_sel;
|
||||
if (st.has_phrase) {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_VIEW) ? UI_MAIN_GENERATE : (ui_main_menu_item_t)((int)st.main_sel + 1);
|
||||
} else {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_ENTER) ? UI_MAIN_GENERATE : UI_MAIN_ENTER;
|
||||
}
|
||||
if ((int)st.main_sel != prev) {
|
||||
draw_main_menu_selection_change(st.has_phrase, prev, (int)st.main_sel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id != BTN_D0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (st.main_sel == UI_MAIN_VIEW) {
|
||||
if (!st.has_phrase) {
|
||||
continue;
|
||||
}
|
||||
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
main_menu_drawn = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (st.main_sel == UI_MAIN_GENERATE) {
|
||||
draw_generate_page();
|
||||
if (generate_mnemonic_12(st.phrase, sizeof(st.phrase)) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "GEN FAILED", RGB565_RED, RGB565_BLACK));
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
main_menu_drawn = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
st.has_phrase = true;
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
|
||||
clear_enter_words(&st);
|
||||
st.letter_idx = 0;
|
||||
|
||||
{
|
||||
prefix_matches_t matches;
|
||||
bool allowed_letters[26];
|
||||
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
|
||||
while (1) {
|
||||
evt = wait_any_button();
|
||||
|
||||
if (evt.id == BTN_D2 && evt.kind == BTN_EVT_PRESS) {
|
||||
if (matches.count == 1 && st.prefix_len > 0) {
|
||||
st.prefix_len = 0;
|
||||
st.prefix[0] = '\0';
|
||||
compute_prefix_matches(&st, &matches);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
} else {
|
||||
int prev_idx = st.letter_idx;
|
||||
st.letter_idx = find_next_allowed_letter(st.letter_idx, -1, allowed_letters);
|
||||
draw_enter_letter_cursor_change(prev_idx, st.letter_idx, allowed_letters);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D1 && evt.kind == BTN_EVT_PRESS) {
|
||||
int prev_idx = st.letter_idx;
|
||||
st.letter_idx = find_next_allowed_letter(st.letter_idx, +1, allowed_letters);
|
||||
draw_enter_letter_cursor_change(prev_idx, st.letter_idx, allowed_letters);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D1 && evt.kind == BTN_EVT_LONG_PRESS) {
|
||||
if (st.prefix_len > 0) {
|
||||
st.prefix_len--;
|
||||
st.prefix[st.prefix_len] = '\0';
|
||||
}
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D2 && evt.kind == BTN_EVT_LONG_PRESS) {
|
||||
if (st.prefix_len > 0) {
|
||||
st.prefix_len = 0;
|
||||
st.prefix[0] = '\0';
|
||||
} else if (st.entered_count > 0) {
|
||||
secure_memzero(st.entered_words[st.entered_count - 1], sizeof(st.entered_words[0]));
|
||||
st.entered_count--;
|
||||
}
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D0 && evt.kind == BTN_EVT_PRESS) {
|
||||
if (st.entered_count >= 12) {
|
||||
if (build_phrase_from_entered(&st) == 0 && mnemonic_validate(st.phrase) == 0) {
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matches.count == 1 && st.prefix_len > 0) {
|
||||
const char *w = mnemonic_word_at(matches.first_idx[0]);
|
||||
if (w != NULL) {
|
||||
strncpy(st.entered_words[st.entered_count], w, 11);
|
||||
st.entered_words[st.entered_count][11] = '\0';
|
||||
st.entered_count++;
|
||||
}
|
||||
st.prefix_len = 0;
|
||||
st.prefix[0] = '\0';
|
||||
|
||||
if (st.entered_count == 12) {
|
||||
if (build_phrase_from_entered(&st) != 0 || mnemonic_validate(st.phrase) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "BAD CHECKSUM", RGB565_RED, RGB565_BLACK));
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (st.prefix_len + 1 < sizeof(st.prefix)) {
|
||||
st.prefix[st.prefix_len++] = (char)('a' + st.letter_idx);
|
||||
st.prefix[st.prefix_len] = '\0';
|
||||
|
||||
compute_prefix_matches(&st, &matches);
|
||||
if (matches.count == 0) {
|
||||
st.prefix_len--;
|
||||
st.prefix[st.prefix_len] = '\0';
|
||||
compute_prefix_matches(&st, &matches);
|
||||
}
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D0 && evt.kind == BTN_EVT_LONG_PRESS) {
|
||||
compute_prefix_matches(&st, &matches);
|
||||
|
||||
if (st.entered_count == 12 && st.prefix_len == 0) {
|
||||
if (build_phrase_from_entered(&st) != 0 || mnemonic_validate(st.phrase) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "BAD CHECKSUM", RGB565_RED, RGB565_BLACK));
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (st.prefix_len == 0) {
|
||||
clear_enter_words(&st);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main_menu_drawn = false;
|
||||
}
|
||||
}
|
||||
13
firmware/feather_s3_tft/main/ui.h
Normal file
13
firmware/feather_s3_tft/main/ui.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int ui_run_until_working(char out_mnemonic[256]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user