#include #include #include #include #include #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "mbedtls/sha256.h" #include "cJSON.h" #include "secp256k1.h" #include "secp256k1_extrakeys.h" #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 #define GIT_HASH "dev" #endif static const char *TAG = "nsigner"; #define APPROVAL_TIMEOUT_MS 30000 typedef enum { APPROVAL_DENY = 0, APPROVAL_ONCE = 1, APPROVAL_ALWAYS = 2, APPROVAL_TIMEOUT = 3, } approval_decision_t; static bool s_always_allow_sign = false; #define AUTH_REQUIRED 1 #define AUTH_KIND 27235 #define AUTH_NONCE_CACHE 32 static uint8_t s_nonce_cache[AUTH_NONCE_CACHE][32]; static int s_nonce_count = 0; static int s_nonce_next = 0; #define AUTH_ERR_ENVELOPE_MALFORMED 2010 #define AUTH_ERR_BODY_MISMATCH 2011 #define AUTH_ERR_SIGNATURE_INVALID 2012 #define AUTH_ERR_KIND_INVALID 2013 #define AUTH_ERR_ENVELOPE_REQUIRED 2014 #define AUTH_ERR_REPLAY_DETECTED 2017 /* Keep large buffers off the main task stack (ESP32-S3 default stack is limited). */ static char s_mnemonic[256]; static uint8_t s_seed[64]; static uint8_t s_privkey[32]; static uint8_t s_pubkey[32]; static char s_npub[128]; static char s_npub_short[40]; static char s_mnemonic_short[64]; static uint8_t s_frame_buf[4096]; 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 approval_decision_t wait_for_approval(uint32_t timeout_ms) { btn_event_t evt = buttons_wait(timeout_ms); if (evt.kind == BTN_EVT_NONE) { return APPROVAL_TIMEOUT; } 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; } static void show_approval_prompt_with_caller(const cJSON *event_in, const char *caller_pubkey_hex) { char line_caller[32]; char line_kind[32]; char line_content[64]; int kind = 1; const char *content = ""; const cJSON *kind_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "kind"); const cJSON *content_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "content"); if (cJSON_IsNumber(kind_item)) { kind = kind_item->valueint; } if (cJSON_IsString(content_item) && content_item->valuestring != NULL) { content = content_item->valuestring; } if (caller_pubkey_hex != NULL && strlen(caller_pubkey_hex) >= 12) { snprintf(line_caller, sizeof(line_caller), "from:%.6s..%.4s", caller_pubkey_hex, caller_pubkey_hex + strlen(caller_pubkey_hex) - 4); } else { snprintf(line_caller, sizeof(line_caller), "from:%.20s", caller_pubkey_hex ? caller_pubkey_hex : "anon"); } snprintf(line_kind, sizeof(line_kind), "kind: %d", kind); snprintf(line_content, sizeof(line_content), "%.50s", content); display_clear(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_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_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_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) { return cdc_recv_frame(payload, payload_max, out_len); } static int send_frame_stdout(const uint8_t *payload, size_t len) { return cdc_send_frame(payload, len); } static void format_npub_short(const char *npub, char *out, size_t out_len) { size_t len; if (out == NULL || out_len == 0) { return; } if (npub == NULL) { out[0] = '\0'; return; } len = strlen(npub); if (len <= 22) { memcpy(out, npub, len + 1); return; } snprintf(out, out_len, "%.11s...%s", npub, npub + len - 6); } static void format_mnemonic_short(const char *mnemonic, char *out, size_t out_len) { size_t copied = 0; int spaces_seen = 0; if (out == NULL || out_len == 0) { return; } out[0] = '\0'; if (mnemonic == NULL) { return; } while (*mnemonic != '\0' && copied + 1 < out_len) { out[copied++] = *mnemonic; if (*mnemonic == ' ') { spaces_seen++; if (spaces_seen >= 3) { break; } } mnemonic++; } if (copied >= 4 && copied + 3 < out_len) { out[copied++] = '.'; out[copied++] = '.'; out[copied++] = '.'; } out[copied] = '\0'; } static void bytes_to_hex(const uint8_t *in, size_t in_len, char *out, size_t out_len) { static const char hex[] = "0123456789abcdef"; size_t i; if (in == NULL || out == NULL || out_len < (in_len * 2U + 1U)) { return; } for (i = 0; i < in_len; ++i) { out[i * 2U] = hex[(in[i] >> 4) & 0x0F]; out[i * 2U + 1U] = hex[in[i] & 0x0F]; } out[in_len * 2U] = '\0'; } static int extract_json_id_token(const char *json, char *out, size_t out_len) { const char *id_pos; const char *p; size_t n = 0; if (json == NULL || out == NULL || out_len < 5) { return -1; } id_pos = strstr(json, "\"id\""); if (id_pos == NULL) { return -1; } p = strchr(id_pos, ':'); if (p == NULL) { return -1; } ++p; while (*p != '\0' && isspace((unsigned char)*p)) { ++p; } if (*p == '\"') { out[n++] = '\"'; ++p; while (*p != '\0' && *p != '\"' && n + 2 < out_len) { out[n++] = *p++; } if (*p != '\"') { return -1; } out[n++] = '\"'; out[n] = '\0'; return 0; } while (*p != '\0' && *p != ',' && *p != '}' && !isspace((unsigned char)*p) && n + 1 < out_len) { out[n++] = *p++; } if (n == 0) { return -1; } out[n] = '\0'; return 0; } static int extract_json_string_field(const char *json, const char *field, char *out, size_t out_len) { char key[48]; const char *pos; const char *p; size_t n = 0; if (json == NULL || field == NULL || out == NULL || out_len < 2) { return -1; } snprintf(key, sizeof(key), "\"%s\"", field); pos = strstr(json, key); if (pos == NULL) { return -1; } p = strchr(pos, ':'); if (p == NULL) { return -1; } ++p; while (*p != '\0' && isspace((unsigned char)*p)) { ++p; } if (*p != '\"') { return -1; } ++p; while (*p != '\0' && *p != '\"' && n + 1 < out_len) { out[n++] = *p++; } if (*p != '\"' || n == 0) { return -1; } out[n] = '\0'; return 0; } static int sha256_bytes(const uint8_t *in, size_t in_len, uint8_t out32[32]) { if (in == NULL || out32 == NULL) { return -1; } mbedtls_sha256(in, in_len, out32, 0); return 0; } static int build_signed_event_json(const cJSON *event_in, const char *pubkey_hex, const uint8_t privkey[32], char *out, size_t out_len) { const cJSON *kind_item; const cJSON *created_item; const cJSON *content_item; const cJSON *tags_item; int kind = 1; int64_t created_at = 0; const char *content = ""; cJSON *ser_arr = NULL; cJSON *result_obj = NULL; cJSON *tags_dup = NULL; char *ser_json = NULL; char *result_json = NULL; uint8_t id32[32] = {0}; uint8_t sig64[64] = {0}; char id_hex[65] = {0}; char sig_hex[129] = {0}; int rc = -1; if (event_in == NULL || pubkey_hex == NULL || privkey == NULL || out == NULL || out_len == 0) { return -1; } kind_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "kind"); created_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "created_at"); content_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "content"); tags_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "tags"); if (cJSON_IsNumber(kind_item)) { kind = kind_item->valueint; } if (cJSON_IsNumber(created_item)) { created_at = (int64_t)created_item->valuedouble; } if (cJSON_IsString(content_item) && content_item->valuestring != NULL) { content = content_item->valuestring; } ser_arr = cJSON_CreateArray(); if (ser_arr == NULL) { goto cleanup; } cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(pubkey_hex)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber((double)created_at)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(kind)); if (cJSON_IsArray(tags_item)) { tags_dup = cJSON_Duplicate((cJSON *)tags_item, 1); } else { tags_dup = cJSON_CreateArray(); } if (tags_dup == NULL) { goto cleanup; } cJSON_AddItemToArray(ser_arr, tags_dup); tags_dup = NULL; cJSON_AddItemToArray(ser_arr, cJSON_CreateString(content)); ser_json = cJSON_PrintUnformatted(ser_arr); if (ser_json == NULL) { goto cleanup; } if (sha256_bytes((const uint8_t *)ser_json, strlen(ser_json), id32) != 0) { goto cleanup; } bytes_to_hex(id32, sizeof(id32), id_hex, sizeof(id_hex)); if (schnorr_sign32(privkey, id32, sig64) != 0) { goto cleanup; } bytes_to_hex(sig64, sizeof(sig64), sig_hex, sizeof(sig_hex)); result_obj = cJSON_CreateObject(); if (result_obj == NULL) { goto cleanup; } cJSON_AddStringToObject(result_obj, "id", id_hex); cJSON_AddStringToObject(result_obj, "pubkey", pubkey_hex); cJSON_AddNumberToObject(result_obj, "created_at", (double)created_at); cJSON_AddNumberToObject(result_obj, "kind", kind); if (cJSON_IsArray(tags_item)) { cJSON_AddItemToObject(result_obj, "tags", cJSON_Duplicate((cJSON *)tags_item, 1)); } else { cJSON_AddItemToObject(result_obj, "tags", cJSON_CreateArray()); } cJSON_AddStringToObject(result_obj, "content", content); cJSON_AddStringToObject(result_obj, "sig", sig_hex); result_json = cJSON_PrintUnformatted(result_obj); if (result_json == NULL) { goto cleanup; } if (strlen(result_json) + 1 > out_len) { goto cleanup; } memcpy(out, result_json, strlen(result_json) + 1); rc = 0; cleanup: if (ser_arr != NULL) { cJSON_Delete(ser_arr); } if (result_obj != NULL) { cJSON_Delete(result_obj); } if (tags_dup != NULL) { cJSON_Delete(tags_dup); } if (ser_json != NULL) { cJSON_free(ser_json); } if (result_json != NULL) { cJSON_free(result_json); } memset(id32, 0, sizeof(id32)); memset(sig64, 0, sizeof(sig64)); return rc; } static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) { size_t hex_len; size_t i; if (hex == NULL || out == NULL) { return -1; } hex_len = strlen(hex); if (hex_len != out_len * 2U) { return -1; } for (i = 0; i < out_len; ++i) { char hi = hex[i * 2U]; char lo = hex[i * 2U + 1U]; int hv, lv; if (hi >= '0' && hi <= '9') hv = hi - '0'; else if (hi >= 'a' && hi <= 'f') hv = 10 + (hi - 'a'); else if (hi >= 'A' && hi <= 'F') hv = 10 + (hi - 'A'); else return -1; if (lo >= '0' && lo <= '9') lv = lo - '0'; else if (lo >= 'a' && lo <= 'f') lv = 10 + (lo - 'a'); else if (lo >= 'A' && lo <= 'F') lv = 10 + (lo - 'A'); else return -1; out[i] = (uint8_t)((hv << 4) | lv); } return 0; } static int auth_replay_check_and_record(const uint8_t id32[32]) { int i; int slot; for (i = 0; i < s_nonce_count; ++i) { if (memcmp(s_nonce_cache[i], id32, 32) == 0) { return -1; } } slot = s_nonce_next; memcpy(s_nonce_cache[slot], id32, 32); s_nonce_next = (s_nonce_next + 1) % AUTH_NONCE_CACHE; if (s_nonce_count < AUTH_NONCE_CACHE) { s_nonce_count++; } return 0; } /* * Verify the auth envelope for an inbound JSON-RPC request. * * Returns: * 0 -> envelope valid, caller_pubkey_hex_out filled in (65 bytes incl. NUL) * >0 -> error code (matches AUTH_ERR_* constants) * * PoC-grade: no wall-clock skew check (no RTC). Replay protection via * in-memory nonce cache of recent auth event ids. */ static int auth_envelope_verify(cJSON *req, const char *method, char caller_pubkey_hex_out[65]) { cJSON *auth_obj = cJSON_GetObjectItemCaseSensitive(req, "auth"); cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params"); cJSON *id_item; cJSON *pk_item; cJSON *created_item; cJSON *kind_item; cJSON *tags_item; cJSON *content_item; cJSON *sig_item; cJSON *ser_arr = NULL; char *ser_json = NULL; char *params_json = NULL; char body_hash_hex[65]; uint8_t computed_id[32]; uint8_t expected_id[32]; uint8_t pubkey32[32]; uint8_t sig64[64]; secp256k1_xonly_pubkey xonly; secp256k1_context *ctx = NULL; int rc = AUTH_ERR_ENVELOPE_MALFORMED; int found_method = 0; int found_body = 0; cJSON *tag_entry; int kind_value; if (auth_obj == NULL || !cJSON_IsObject(auth_obj)) { return AUTH_ERR_ENVELOPE_REQUIRED; } id_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "id"); pk_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "pubkey"); created_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "created_at"); kind_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "kind"); tags_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "tags"); content_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "content"); sig_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "sig"); if (!cJSON_IsString(id_item) || !cJSON_IsString(pk_item) || !cJSON_IsNumber(created_item) || !cJSON_IsNumber(kind_item) || !cJSON_IsArray(tags_item) || !cJSON_IsString(content_item) || !cJSON_IsString(sig_item)) { return AUTH_ERR_ENVELOPE_MALFORMED; } kind_value = kind_item->valueint; if (kind_value != AUTH_KIND) { return AUTH_ERR_KIND_INVALID; } if (hex_to_bytes(id_item->valuestring, expected_id, 32) != 0 || hex_to_bytes(pk_item->valuestring, pubkey32, 32) != 0 || hex_to_bytes(sig_item->valuestring, sig64, 64) != 0) { return AUTH_ERR_ENVELOPE_MALFORMED; } ser_arr = cJSON_CreateArray(); if (ser_arr == NULL) { return AUTH_ERR_ENVELOPE_MALFORMED; } cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(pk_item->valuestring)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(created_item->valuedouble)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(kind_value)); cJSON_AddItemToArray(ser_arr, cJSON_Duplicate(tags_item, 1)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(content_item->valuestring)); ser_json = cJSON_PrintUnformatted(ser_arr); if (ser_json == NULL) { rc = AUTH_ERR_ENVELOPE_MALFORMED; goto done; } mbedtls_sha256((const uint8_t *)ser_json, strlen(ser_json), computed_id, 0); if (memcmp(computed_id, expected_id, 32) != 0) { rc = AUTH_ERR_ENVELOPE_MALFORMED; goto done; } ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); if (ctx == NULL) { rc = AUTH_ERR_SIGNATURE_INVALID; goto done; } if (!secp256k1_xonly_pubkey_parse(ctx, &xonly, pubkey32)) { rc = AUTH_ERR_SIGNATURE_INVALID; goto done; } if (!secp256k1_schnorrsig_verify(ctx, sig64, expected_id, 32, &xonly)) { rc = AUTH_ERR_SIGNATURE_INVALID; goto done; } { cJSON *params_for_hash = params != NULL ? params : cJSON_CreateArray(); params_json = cJSON_PrintUnformatted(params_for_hash); } if (params_json == NULL) { rc = AUTH_ERR_BODY_MISMATCH; goto done; } { uint8_t body_hash[32]; size_t i; static const char hex[] = "0123456789abcdef"; mbedtls_sha256((const uint8_t *)params_json, strlen(params_json), body_hash, 0); for (i = 0; i < 32; ++i) { body_hash_hex[i * 2U] = hex[(body_hash[i] >> 4) & 0x0F]; body_hash_hex[i * 2U + 1U] = hex[body_hash[i] & 0x0F]; } body_hash_hex[64] = '\0'; } cJSON_ArrayForEach(tag_entry, tags_item) { cJSON *k_item; cJSON *v_item; if (!cJSON_IsArray(tag_entry) || cJSON_GetArraySize(tag_entry) < 2) { continue; } k_item = cJSON_GetArrayItem(tag_entry, 0); v_item = cJSON_GetArrayItem(tag_entry, 1); if (!cJSON_IsString(k_item) || !cJSON_IsString(v_item)) { continue; } if (strcmp(k_item->valuestring, "nsigner_method") == 0 && strcmp(v_item->valuestring, method) == 0) { found_method = 1; } else if (strcmp(k_item->valuestring, "nsigner_body_hash") == 0 && strcasecmp(v_item->valuestring, body_hash_hex) == 0) { found_body = 1; } } if (!found_method || !found_body) { rc = AUTH_ERR_BODY_MISMATCH; goto done; } if (auth_replay_check_and_record(expected_id) != 0) { rc = AUTH_ERR_REPLAY_DETECTED; goto done; } memcpy(caller_pubkey_hex_out, pk_item->valuestring, 64); caller_pubkey_hex_out[64] = '\0'; rc = 0; done: if (ser_arr != NULL) { cJSON_Delete(ser_arr); } if (ser_json != NULL) { cJSON_free(ser_json); } if (params_json != NULL) { cJSON_free(params_json); } if (ctx != NULL) { secp256k1_context_destroy(ctx); } return rc; } static int parse_nostr_index_from_params(cJSON *params, uint32_t *out_index) { int params_count; cJSON *last_item; cJSON *idx_item; int idx; if (out_index == NULL) { return -1; } *out_index = 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)); return -1; } 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)); 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)); 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[0]: %s\n", s_npub); printf("Derived pubkey hex[0]: %s\n", s_pubkey_hex); show_idle_screen(); secure_memzero(s_pubkey, sizeof(s_pubkey)); return 0; } 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]; char stat2[32]; int got_frame = 0; int via_webusb = 0; if (webusb_recv_frame(s_frame_buf, sizeof(s_frame_buf), &last_len) == 0) { got_frame = 1; via_webusb = 1; } else if (recv_frame_stdin(s_frame_buf, sizeof(s_frame_buf), &last_len) == 0) { got_frame = 1; via_webusb = 0; } if (!got_frame) { vTaskDelay(pdMS_TO_TICKS(2)); continue; } 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}; char caller_pubkey_hex[65] = {0}; int auth_rc = 0; cJSON *req = NULL; s_frame_buf[last_len] = '\0'; if (extract_json_id_token((const char *)s_frame_buf, id_token, sizeof(id_token)) != 0) { strncpy(id_token, "null", sizeof(id_token) - 1); } req = cJSON_Parse((const char *)s_frame_buf); if (req == NULL || extract_json_string_field((const char *)s_frame_buf, "method", method, sizeof(method)) != 0) { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32600,\"message\":\"invalid request\"}}", id_token); } else { #if AUTH_REQUIRED auth_rc = auth_envelope_verify(req, method, caller_pubkey_hex); #else auth_rc = 0; strncpy(caller_pubkey_hex, "anon", sizeof(caller_pubkey_hex) - 1); #endif if (auth_rc != 0) { const char *msg = "auth envelope required"; if (auth_rc == AUTH_ERR_ENVELOPE_MALFORMED) msg = "auth envelope malformed"; else if (auth_rc == AUTH_ERR_BODY_MISMATCH) msg = "auth body mismatch"; else if (auth_rc == AUTH_ERR_SIGNATURE_INVALID) msg = "auth signature invalid"; else if (auth_rc == AUTH_ERR_KIND_INVALID) msg = "auth kind invalid"; else if (auth_rc == AUTH_ERR_REPLAY_DETECTED) msg = "auth replay detected"; snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, auth_rc, msg); } else if (strcmp(method, "get_public_key") == 0) { 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) && 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; /* 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, req_pubkey_hex, req_privkey, s_signed_event_buf, sizeof(s_signed_event_buf)) == 0) { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, s_signed_event_buf); } else if (decision == APPROVAL_DENY) { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}", id_token); } else if (decision == APPROVAL_TIMEOUT) { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}", id_token); } else { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}", id_token); } } else { snprintf( s_response_buf, sizeof(s_response_buf), "{\"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, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32601,\"message\":\"method not found\"}}", id_token); } } if (req != NULL) { cJSON_Delete(req); } 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_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(); }