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:
Laan Tungir
2026-05-11 13:43:14 -04:00
parent e4fa743654
commit bd23b674d6
19 changed files with 2514 additions and 222 deletions

View File

@@ -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();
}