diff --git a/.gitignore b/.gitignore index a4a57a64..09bd0a51 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,18 @@ node_modules/ *.dylib *.dll build/ + +# Build outputs/binaries (do not track) +/websocket_debug +/libnostr_core_arm64.a +/tests/websocket_debug +/tests/*_test +/examples/input_detection +/examples/keypair_generation +/examples/mnemonic_derivation +/examples/mnemonic_generation +/examples/relay_pool +/examples/send_nip17_dm +/examples/simple_keygen +/examples/utility_functions +/examples/version_test diff --git a/=5 b/=5 new file mode 100644 index 00000000..e69de29b diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..6110aa5a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,66 @@ +cmake_minimum_required(VERSION 3.16) + +if(ESP_PLATFORM) + idf_component_register( + SRCS + "nostr_core/nostr_common.c" + "nostr_core/nostr_log.c" + "nostr_core/nip001.c" + "nostr_core/nip004.c" + "nostr_core/nip006.c" + "nostr_core/nostr_signer.c" + "nostr_core/nip019.c" + "nostr_core/utils.c" + "nostr_core/crypto/nostr_secp256k1.c" + "nostr_core/crypto/nostr_aes.c" + "nostr_core/crypto/nostr_chacha20.c" + "cjson/cJSON.c" + "platform/esp32/nostr_platform_esp32.c" + "platform/esp32/nostr_http_esp32.c" + "platform/esp32/nostr_websocket_esp32.c" + INCLUDE_DIRS + "." + "nostr_core" + "nostr_core/crypto" + "cjson" + "nostr_websocket" + REQUIRES + secp256k1 + esp_hw_support + esp_http_client + esp-tls + tcp_transport + mbedtls + ) + + target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1) +else() + project(nostr_core_lib C) + + add_library(nostr_core STATIC + nostr_core/nostr_common.c + nostr_core/nostr_log.c + nostr_core/request_validator.c + nostr_core/nip001.c + nostr_core/nip006.c + nostr_core/nip019.c + nostr_core/nostr_signer.c + nostr_core/nsigner_transport.c + nostr_core/nsigner_client.c + nostr_core/utils.c + nostr_core/crypto/nostr_secp256k1.c + nostr_core/crypto/nostr_aes.c + nostr_core/crypto/nostr_chacha20.c + cjson/cJSON.c + platform/linux.c + ) + + target_include_directories(nostr_core PUBLIC + . + nostr_core + nostr_core/crypto + cjson + nostr_websocket + ) + target_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1) +endif() diff --git a/README.md b/README.md index 5554398d..982d9199 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A C library for NOSTR protocol implementation. Work in progress. -[![Version](https://img.shields.io/badge/version-0.5.12-blue.svg)](VERSION) +[![Version](https://img.shields.io/badge/version-0.6.0-blue.svg)](VERSION) [![License](https://img.shields.io/badge/license-MIT-green.svg)](#license) [![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](#building) @@ -346,6 +346,163 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf void nostr_nip11_relay_info_free(nostr_relay_info_t* info); ``` +## Signer abstraction & nsigner integration + +All signing/encryption operations can run through an opaque `nostr_signer_t` handle. This provides: + +- **LOCAL backend** (`nostr_signer_local`) using a raw 32-byte private key (output-equivalent to legacy raw-key APIs). +- **REMOTE backends** (`nostr_signer_nsigner_*`) that call a running `n_signer` process/device over supported transports. + +### Core signer verbs (exact signatures) + +```c +/* Lifecycle */ +nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]); +void nostr_signer_free(nostr_signer_t* signer); + +/* Core verbs */ +int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]); +int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out); + +/* Encryption verbs */ +int nostr_signer_nip04_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out); +int nostr_signer_nip04_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out); +int nostr_signer_nip44_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out); +int nostr_signer_nip44_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out); +``` + +### Backends / transports + +Remote signer factories (available when `NOSTR_ENABLE_NSIGNER_CLIENT` is enabled): + +```c +nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms); +int nostr_signer_nsigner_set_auth(nostr_signer_t* signer, + const unsigned char auth_privkey[32], + const char* label); +``` + +Transport layer constructors and discovery helpers: + +```c +nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms); +nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms); +nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms); +nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms); +int nsigner_transport_list_unix(char names[][64], int max_names); +int nsigner_transport_list_serial(char paths[][64], int max_paths); +``` + +Supported transport modes: +- UNIX abstract socket (`unix:`, no leading `@` in API input) +- USB CDC-ACM serial (`serial:/dev/ttyACM0`) +- TCP (`tcp::`) +- Existing framed fd pair (`fds::`, useful for stdio/qrexec-style bridges) + +Build gating: +- Desktop builds enable `NOSTR_ENABLE_NSIGNER_CLIENT`. +- ESP32 builds exclude the nsigner client transport/client sources and do not define the flag. +- n_signer’s own build keeps this client side disabled. + +Wire contract (high level): +- Framing is **4-byte big-endian length + JSON payload**. +- Payload length must be in `[1, 65536]` bytes. +- TCP uses a kind-27235 auth envelope (`nsigner_rpc`, `nsigner_method`, `nsigner_body_hash`). +- See n_signer docs for authoritative protocol details. + +### Using a signer in higher-level APIs + +Pattern: existing raw-private-key functions remain unchanged; signer-aware siblings are provided as `*_with_signer` variants. + +Covered modules and functions: + +- NIP-01 (`nostr_core/nip001.h`) + - `cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);` + +- NIP-17 (`nostr_core/nip017.h`) + - `cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, int num_relays, nostr_signer_t* signer);` + - `int nostr_nip17_send_dm_with_signer(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, nostr_signer_t* signer, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec);` + - `cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);` + +- NIP-42 (`nostr_core/nip042.h`) + - `cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge, const char* relay_url, nostr_signer_t* signer, time_t timestamp);` + +- NIP-46 client/event helpers (`nostr_core/nip046.h`) + - `cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request, nostr_signer_t* signer, const unsigned char* recipient_public_key, time_t timestamp);` + - `cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response, nostr_signer_t* signer, const unsigned char* recipient_public_key, time_t timestamp);` + - `int nostr_nip46_decrypt_event_with_signer(cJSON* event, nostr_signer_t* signer, char** output_out);` + - `int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session, nostr_signer_t* signer, const char* bunker_url);` + +- NIP-60 (`nostr_core/nip060.h`) + - `cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs, const char** deleted_event_ids, int deleted_count, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id, const char* mint_url, time_t expiration, nostr_signer_t* signer, time_t timestamp);` + +- NIP-61 (`nostr_core/nip061.h`) + - `cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data, nostr_signer_t* signer, time_t timestamp);` + - `cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id, const char* nutzap_relay_hint, const char* sender_pubkey, const char* created_token_event_id, const char* created_token_relay_hint, uint64_t amount, nostr_signer_t* signer, time_t timestamp);` + +- Blossom auth/upload/delete (`nostr_core/blossom_client.h`) + - `char* blossom_create_auth_header_with_signer(nostr_signer_t* signer, const char* operation, const char* sha256_hex, int expiration_seconds, time_t timestamp);` + - `int blossom_upload_with_signer(const char* server_url, const unsigned char* data, size_t data_len, const char* content_type, nostr_signer_t* signer, const char* sha256_hex, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out);` + - `int blossom_upload_file_with_signer(const char* server_url, const char* file_path, const char* content_type, nostr_signer_t* signer, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out);` + - `int blossom_delete_with_signer(const char* server_url, const char* sha256_hex, nostr_signer_t* signer, int timeout_seconds);` + +- Relay-pool NIP-42 AUTH entry point (`nostr_core/nostr_core.h`) + - `int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);` + +### Driver app: `examples/note_poster` + +Signer modes: + +```bash +./examples/note_poster +``` + +Default is local signer mode; this prompts for `nsec1...` or 64-char hex private key. + +**WARNING:** test-key-only flow. Do not use a production key; input key material is held in process memory in plaintext. + +```bash +./examples/note_poster --signer unix: +./examples/note_poster --signer serial:/dev/ttyACM0 +./examples/note_poster --signer tcp:: +./examples/note_poster --signer fds:: +``` + +### Boundaries / what stays in n_signer + +`nostr_core_lib` provides the **client side only** (transports + nsigner client + signer backends). The following stay in the `n_signer` project: + +- process spawning/embedding and deployment packaging +- signer program trust-boundary features (mnemonic handling, approvals, role policy) +- hardware firmware and platform/device-specific runtime pieces + +Dependency direction remains one-way: **`n_signer -> nostr_core_lib`**. + +Operations requiring raw secret material outside signer verbs are not remotable as-is. Example: mint-protocol operations in Cashu mint pieces (`cashu_mint`) that require direct secret access beyond event signing/NIP-04/NIP-44. + +For implementation plan context see `plans/nsigner_integration_plan.md`. This integration supersedes per-project hand-rolled signer clients (future migration milestone M7). + ## πŸ“ Examples The library includes comprehensive examples: @@ -508,6 +665,20 @@ int main(void) { - **Windows** (MinGW, MSYS2) - **Embedded Systems** (resource-constrained environments) +## πŸ”Œ Embedded (ESP32) Support + +`nostr_core_lib` now includes a platform abstraction layer and ESP32-native implementations for core networking and entropy APIs, while preserving desktop compatibility. + +Implemented embedded capabilities include: +- platform random source abstraction via `nostr_platform_random()` +- ESP32 random provider using `esp_fill_random` +- ESP32 HTTP client implementation via `esp_http_client` +- ESP32 WebSocket/WSS transport via `esp_transport_ws` + TLS certificate bundle +- NIP-04 encrypted DM flow validated on-device against public relays + +Reference ESP-IDF example project (in this workspace): +- `../esp32_send_kind4/` β€” Wi-Fi connect + `wss://` relay connect + send two kind-4 encrypted events + print relay responses + ## πŸ“„ Documentation - **[POOL_API.md](POOL_API.md)** - Relay pool API notes @@ -527,20 +698,22 @@ int main(void) { ## πŸ“ˆ Version History -Current version: **0.5.12** +Current version: **0.6.0** The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically. **Recent Developments:** -- **OpenSSL Migration**: Transitioned from mbedTLS to OpenSSL for improved compatibility +- **ESP32 Embedded Enablement**: Added PAL-based embedded support directly in `nostr_core_lib` +- **ESP32 HTTP + WSS**: Added ESP-IDF-backed HTTP and secure websocket transport implementations +- **NIP-04 On-Device Validation**: Verified encrypted kind-4 DM publish flow from ESP32 to live relays - **NIP-05 Support**: DNS-based internet identifier verification - **NIP-11 Support**: Relay information document fetching and parsing - **NIP-19 Support**: Bech32-encoded entities (nsec/npub) -- **Enhanced WebSocket**: OpenSSL-based TLS WebSocket communication - **Comprehensive Testing**: Extensive test suite and error handling **Version Timeline:** -- `v0.4.x` - Current development releases with relay pool and expanded NIP support +- `v0.6.x` - Embedded-capable releases with ESP32 PAL, HTTP, and WSS support +- `v0.4.x` - Development releases with relay pool and expanded NIP support - `v0.2.x` - Earlier OpenSSL-based development releases - `v0.1.x` - Initial development releases - Focus on core protocol implementation and OpenSSL-based crypto diff --git a/UPSTREAM_VERSION b/UPSTREAM_VERSION new file mode 100644 index 00000000..05e8a459 --- /dev/null +++ b/UPSTREAM_VERSION @@ -0,0 +1 @@ +0.6.6 diff --git a/VERSION b/VERSION index 3afb327e..05e8a459 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.16 +0.6.6 diff --git a/backups/pre-filter-backup.bundle b/backups/pre-filter-backup.bundle new file mode 100644 index 00000000..9f54f577 Binary files /dev/null and b/backups/pre-filter-backup.bundle differ diff --git a/build.sh b/build.sh index 21024c29..7ae3adaa 100755 --- a/build.sh +++ b/build.sh @@ -500,15 +500,21 @@ detect_system_curl SOURCES="nostr_core/crypto/nostr_secp256k1.c" SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c" SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c" +SOURCES="$SOURCES nostr_core/crypto/nostr_poly1305.c" +SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20poly1305.c" SOURCES="$SOURCES cjson/cJSON.c" SOURCES="$SOURCES nostr_core/utils.c" SOURCES="$SOURCES nostr_core/nostr_common.c" +SOURCES="$SOURCES nostr_core/nostr_signer.c" SOURCES="$SOURCES nostr_core/core_relays.c" SOURCES="$SOURCES nostr_core/core_relay_pool.c" SOURCES="$SOURCES nostr_core/nostr_log.c" SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c" SOURCES="$SOURCES nostr_core/request_validator.c" SOURCES="$SOURCES nostr_core/nostr_http.c" +SOURCES="$SOURCES platform/linux.c" +SOURCES="$SOURCES nostr_core/nsigner_transport.c" +SOURCES="$SOURCES nostr_core/nsigner_client.c" NIP_DESCRIPTIONS="" @@ -548,6 +554,7 @@ SOURCES="$SOURCES nostr_core/blossom_client.c" # Build flags CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2" CFLAGS="$CFLAGS -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING" +CFLAGS="$CFLAGS -DNOSTR_ENABLE_NSIGNER_CLIENT=1" INCLUDES="-I. -Inostr_core -Inostr_core/crypto -Icjson -Inostr_websocket" # Add system library includes diff --git a/examples/cashu_wallet b/examples/cashu_wallet new file mode 100755 index 00000000..34111e2a Binary files /dev/null and b/examples/cashu_wallet differ diff --git a/examples/input_detection b/examples/input_detection index c0a2bb6e..065c15b1 100755 Binary files a/examples/input_detection and b/examples/input_detection differ diff --git a/examples/keypair_generation b/examples/keypair_generation index 82fe2271..8096271e 100755 Binary files a/examples/keypair_generation and b/examples/keypair_generation differ diff --git a/examples/mnemonic_derivation b/examples/mnemonic_derivation index 3995a5c7..c4e866bf 100755 Binary files a/examples/mnemonic_derivation and b/examples/mnemonic_derivation differ diff --git a/examples/mnemonic_generation b/examples/mnemonic_generation index e88268f8..ac3d3c64 100755 Binary files a/examples/mnemonic_generation and b/examples/mnemonic_generation differ diff --git a/examples/nip46_remote_signer b/examples/nip46_remote_signer new file mode 100755 index 00000000..9dc4c897 Binary files /dev/null and b/examples/nip46_remote_signer differ diff --git a/examples/note_poster b/examples/note_poster new file mode 100755 index 00000000..38c1d193 Binary files /dev/null and b/examples/note_poster differ diff --git a/examples/note_poster.c b/examples/note_poster.c new file mode 100644 index 00000000..49acad4f --- /dev/null +++ b/examples/note_poster.c @@ -0,0 +1,300 @@ +/* + * note_poster - minimal sign-in + kind-1 post test app + * + * Supports signer modes: + * - local (default): sign in with a local key (nsec or hex private key) + * - unix:: use remote nsigner over AF_UNIX abstract socket + * - serial:: use remote nsigner over USB CDC serial (e.g. /dev/ttyACM0) + * - tcp::: use remote nsigner over TCP (auth envelope required by n_signer) + * - fds::: use remote nsigner over existing framed fd pair (stdio/qrexec helper) + * + * Creates and signs a kind-1 note through nostr_signer_t, + * then publishes to wss://relay.laantungir.net + */ + +#include "../nostr_core/nostr_core.h" +#include +#include +#include +#include + +#define DEFAULT_RELAY "wss://relay.laantungir.net" + +static void publish_progress_callback(const char* relay_url, + const char* status, + const char* message, + int success_count, + int total_relays, + int completed_relays, + void* user_data) { + (void)user_data; + + if (relay_url) { + printf("[%s] %s", relay_url, status ? status : "(unknown)"); + if (message) { + printf(" - %s", message); + } + printf(" (%d/%d complete, %d success)\n", completed_relays, total_relays, success_count); + } +} + +static int read_line(const char* prompt, char* out, size_t out_sz) { + size_t len; + if (!prompt || !out || out_sz == 0) { + return -1; + } + + printf("%s", prompt); + if (!fgets(out, (int)out_sz, stdin)) { + return -1; + } + + len = strlen(out); + if (len > 0 && out[len - 1] == '\n') { + out[len - 1] = '\0'; + } + + return 0; +} + +static int parse_private_key_input(const char* input, unsigned char out_privkey[32]) { + if (!input || !out_privkey) { + return -1; + } + + if (strncmp(input, "nsec1", 5) == 0) { + return nostr_decode_nsec(input, out_privkey); + } + + if (strlen(input) == 64) { + return nostr_hex_to_bytes(input, out_privkey, 32); + } + + return -1; +} + +int main(int argc, char* argv[]) { + unsigned char private_key[32] = {0}; + char key_input[256] = {0}; + char note_content[2048] = {0}; + nostr_signer_t* signer = NULL; + char pubkey_hex[65] = {0}; + cJSON* note_event = NULL; + cJSON* id_item; + const char* relay_urls[] = {DEFAULT_RELAY}; + int success_count = 0; + publish_result_t* results = NULL; + enum { + SIGNER_MODE_LOCAL = 0, + SIGNER_MODE_UNIX = 1, + SIGNER_MODE_SERIAL = 2, + SIGNER_MODE_TCP = 3, + SIGNER_MODE_FDS = 4 + } signer_mode = SIGNER_MODE_LOCAL; + const char* unix_name = NULL; + const char* serial_path = NULL; + char tcp_host[256] = {0}; + int tcp_port = 0; + int fds_read_fd = -1; + int fds_write_fd = -1; + int argi = 1; + + if (argc >= 3 && strcmp(argv[1], "--signer") == 0) { + if (strncmp(argv[2], "unix:", 5) == 0 && argv[2][5] != '\0') { + signer_mode = SIGNER_MODE_UNIX; + unix_name = argv[2] + 5; + } else if (strncmp(argv[2], "serial:", 7) == 0 && argv[2][7] != '\0') { + signer_mode = SIGNER_MODE_SERIAL; + serial_path = argv[2] + 7; + } else if (strncmp(argv[2], "tcp:", 4) == 0 && argv[2][4] != '\0') { + const char* spec = argv[2] + 4; + const char* sep = strrchr(spec, ':'); + char* endptr = NULL; + long parsed_port; + size_t host_len; + + if (sep == NULL || sep == spec || sep[1] == '\0') { + fprintf(stderr, "Invalid tcp signer format. Use tcp::\n"); + return 1; + } + + host_len = (size_t)(sep - spec); + if (host_len >= sizeof(tcp_host)) { + fprintf(stderr, "TCP host too long\n"); + return 1; + } + + memcpy(tcp_host, spec, host_len); + tcp_host[host_len] = '\0'; + + parsed_port = strtol(sep + 1, &endptr, 10); + if (endptr == NULL || *endptr != '\0' || parsed_port <= 0 || parsed_port > 65535) { + fprintf(stderr, "Invalid TCP port in --signer tcp::\n"); + return 1; + } + + signer_mode = SIGNER_MODE_TCP; + tcp_port = (int)parsed_port; + } else if (strncmp(argv[2], "fds:", 4) == 0 && argv[2][4] != '\0') { + int parsed = -1; + parsed = sscanf(argv[2] + 4, "%d:%d", &fds_read_fd, &fds_write_fd); + if (parsed != 2 || fds_read_fd < 0 || fds_write_fd < 0) { + fprintf(stderr, "Invalid fds signer format. Use fds::\n"); + return 1; + } + signer_mode = SIGNER_MODE_FDS; + } else if (strcmp(argv[2], "local") == 0) { + signer_mode = SIGNER_MODE_LOCAL; + } else { + fprintf(stderr, + "Invalid --signer value. Use --signer local, --signer unix:, --signer serial:, --signer tcp::, or --signer fds::\n"); + return 1; + } + argi = 3; + } + + if (signer_mode == SIGNER_MODE_LOCAL) { + if (argc > argi) { + strncpy(key_input, argv[argi], sizeof(key_input) - 1); + argi++; + } else if (read_line("Enter nsec or 64-char hex private key: ", key_input, sizeof(key_input)) != 0) { + fprintf(stderr, "Failed to read key input\n"); + return 1; + } + + fprintf(stderr, "WARNING: This is a TEST-KEY-ONLY tool. Do not use a real/production nsec. Keys are read into process memory in plaintext.\n"); + if (parse_private_key_input(key_input, private_key) != 0) { + fprintf(stderr, "Invalid private key input (must be nsec1... or 64-char hex)\n"); + return 1; + } + } + + if (argc > argi) { + strncpy(note_content, argv[argi], sizeof(note_content) - 1); + } else if (read_line("Enter note content: ", note_content, sizeof(note_content)) != 0) { + fprintf(stderr, "Failed to read note content\n"); + return 1; + } + + if (nostr_init() != NOSTR_SUCCESS) { + fprintf(stderr, "Failed to initialize nostr library\n"); + return 1; + } + + if (signer_mode == SIGNER_MODE_UNIX) { +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + signer = nostr_signer_nsigner_unix(unix_name, NULL, 15000); + if (!signer) { + fprintf(stderr, "Failed to initialize unix nsigner backend (%s)\n", unix_name); + nostr_cleanup(); + return 1; + } +#else + fprintf(stderr, "This build does not include nsigner client support\n"); + nostr_cleanup(); + return 1; +#endif + } else if (signer_mode == SIGNER_MODE_SERIAL) { +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + signer = nostr_signer_nsigner_serial(serial_path, NULL, 15000); + if (!signer) { + fprintf(stderr, "Failed to initialize serial nsigner backend (%s)\n", serial_path); + nostr_cleanup(); + return 1; + } +#else + fprintf(stderr, "This build does not include nsigner client support\n"); + nostr_cleanup(); + return 1; +#endif + } else if (signer_mode == SIGNER_MODE_TCP) { +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + signer = nostr_signer_nsigner_tcp(tcp_host, tcp_port, NULL, 15000); + if (!signer) { + fprintf(stderr, "Failed to initialize tcp nsigner backend (%s:%d)\n", tcp_host, tcp_port); + nostr_cleanup(); + return 1; + } +#else + fprintf(stderr, "This build does not include nsigner client support\n"); + nostr_cleanup(); + return 1; +#endif + } else if (signer_mode == SIGNER_MODE_FDS) { +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + signer = nostr_signer_nsigner_fds(fds_read_fd, fds_write_fd, NULL, 15000); + if (!signer) { + fprintf(stderr, "Failed to initialize fds nsigner backend (%d:%d)\n", fds_read_fd, fds_write_fd); + nostr_cleanup(); + return 1; + } +#else + fprintf(stderr, "This build does not include nsigner client support\n"); + nostr_cleanup(); + return 1; +#endif + } else { + signer = nostr_signer_local(private_key); + if (!signer) { + fprintf(stderr, "Failed to initialize local signer\n"); + nostr_cleanup(); + return 1; + } + } + + if (nostr_signer_get_public_key(signer, pubkey_hex) != NOSTR_SUCCESS) { + fprintf(stderr, "Failed to get signer pubkey\n"); + nostr_signer_free(signer); + nostr_cleanup(); + return 1; + } + + printf("Signed in as pubkey: %s\n", pubkey_hex); + + note_event = nostr_create_and_sign_event_with_signer(1, note_content, NULL, signer, time(NULL)); + if (!note_event) { + fprintf(stderr, "Failed to create/sign note event\n"); + nostr_signer_free(signer); + nostr_cleanup(); + return 1; + } + + id_item = cJSON_GetObjectItem(note_event, "id"); + if (id_item && cJSON_IsString(id_item)) { + printf("Created event id: %s\n", cJSON_GetStringValue(id_item)); + } + + printf("Publishing to %s ...\n", DEFAULT_RELAY); + results = synchronous_publish_event_with_progress( + relay_urls, + 1, + note_event, + &success_count, + 12, + publish_progress_callback, + NULL, + 0, + NULL + ); + + if (!results || success_count < 1 || results[0] != PUBLISH_SUCCESS) { + fprintf(stderr, "Publish failed (success_count=%d, result=%d)\n", + success_count, + results ? (int)results[0] : -999); + if (results) { + free(results); + } + cJSON_Delete(note_event); + nostr_signer_free(signer); + nostr_cleanup(); + return 1; + } + + printf("βœ… Published successfully to %s\n", DEFAULT_RELAY); + + free(results); + cJSON_Delete(note_event); + nostr_signer_free(signer); + nostr_cleanup(); + return 0; +} diff --git a/examples/relay_pool b/examples/relay_pool index 157ec0f8..14ec40a5 100755 Binary files a/examples/relay_pool and b/examples/relay_pool differ diff --git a/examples/send_nip17_dm b/examples/send_nip17_dm index e747733a..f39b8199 100755 Binary files a/examples/send_nip17_dm and b/examples/send_nip17_dm differ diff --git a/examples/send_nip17_dm.c b/examples/send_nip17_dm.c index b07db28a..a3aec3e8 100644 --- a/examples/send_nip17_dm.c +++ b/examples/send_nip17_dm.c @@ -187,7 +187,8 @@ int main(int argc, char* argv[]) { 1, sender_privkey, gift_wraps, - 10 + 10, + 0 ); cJSON_Delete(dm_event); // Original DM event no longer needed diff --git a/examples/simple_keygen b/examples/simple_keygen index d315138f..4564147a 100755 Binary files a/examples/simple_keygen and b/examples/simple_keygen differ diff --git a/examples/utility_functions b/examples/utility_functions index 1581e94a..66897a5c 100755 Binary files a/examples/utility_functions and b/examples/utility_functions differ diff --git a/examples/version_test b/examples/version_test index d057f4ff..7fe606a6 100755 Binary files a/examples/version_test and b/examples/version_test differ diff --git a/increment_and_push.sh b/increment_and_push.sh index a9a33636..075a2408 100755 --- a/increment_and_push.sh +++ b/increment_and_push.sh @@ -1,7 +1,9 @@ #!/bin/bash # increment_and_push.sh - Version increment and git automation script -# Usage: ./increment_and_push.sh "meaningful git comment" +# Usage: +# ./increment_and_push.sh "meaningful git comment" +# ./increment_and_push.sh --set-version vX.Y.Z "meaningful git comment" set -e # Exit on error @@ -65,16 +67,30 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then exit 1 fi -# Check if we have a commit message -if [ $# -eq 0 ]; then - print_error "Usage: $0 \"meaningful git comment\"" - echo "" - echo "Example: $0 \"Add enhanced subscription functionality\"" - echo "" - exit 1 -fi +TARGET_VERSION="" +COMMIT_MESSAGE="" -COMMIT_MESSAGE="$1" +# Parse arguments +if [ "$1" = "--set-version" ]; then + if [ $# -lt 3 ]; then + print_error "Usage: $0 --set-version vX.Y.Z \"meaningful git comment\"" + echo "" + echo "Example: $0 --set-version v0.6.0 \"Release v0.6.0 with ESP32 embedded support\"" + echo "" + exit 1 + fi + TARGET_VERSION="$2" + COMMIT_MESSAGE="$3" +else + if [ $# -eq 0 ]; then + print_error "Usage: $0 \"meaningful git comment\"" + echo "" + echo "Example: $0 \"Add enhanced subscription functionality\"" + echo "" + exit 1 + fi + COMMIT_MESSAGE="$1" +fi # Check if nostr_core.h exists if [ ! -f "nostr_core/nostr_core.h" ]; then @@ -103,21 +119,37 @@ fi print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_PATCH)" -# Increment patch version -NEW_PATCH=$((VERSION_PATCH + 1)) -NEW_VERSION="v$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" +if [ -n "$TARGET_VERSION" ]; then + if [[ ! "$TARGET_VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + print_error "Invalid target version format: $TARGET_VERSION (expected vX.Y.Z)" + exit 1 + fi + + NEW_VERSION="$TARGET_VERSION" + NEW_MAJOR="${BASH_REMATCH[1]}" + NEW_MINOR="${BASH_REMATCH[2]}" + NEW_PATCH="${BASH_REMATCH[3]}" +else + # Increment patch version + NEW_MAJOR="$VERSION_MAJOR" + NEW_MINOR="$VERSION_MINOR" + NEW_PATCH=$((VERSION_PATCH + 1)) + NEW_VERSION="v$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" +fi print_info "New version will be: $NEW_VERSION" # Update version in nostr_core.h sed -i "s/#define VERSION .*/#define VERSION \"$NEW_VERSION\"/" nostr_core/nostr_core.h +sed -i "s/#define VERSION_MAJOR .*/#define VERSION_MAJOR $NEW_MAJOR/" nostr_core/nostr_core.h +sed -i "s/#define VERSION_MINOR .*/#define VERSION_MINOR $NEW_MINOR/" nostr_core/nostr_core.h sed -i "s/#define VERSION_PATCH .*/#define VERSION_PATCH $NEW_PATCH/" nostr_core/nostr_core.h print_success "Updated version in nostr_core.h" # Check if VERSION file exists and update it if [ -f "VERSION" ]; then - echo "$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" > VERSION + echo "$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" > VERSION print_success "Updated VERSION file" fi diff --git a/nostr_core/NSIGNER_INTEGRATION.md b/nostr_core/NSIGNER_INTEGRATION.md new file mode 100644 index 00000000..4808b1d3 --- /dev/null +++ b/nostr_core/NSIGNER_INTEGRATION.md @@ -0,0 +1,467 @@ +# NSIGNER Integration in `nostr_core_lib` + +This document is the developer-facing integration contract for signer abstraction + nsigner client support in `nostr_core_lib`. + +For the concise overview, see the signer section in `README.md`. +For implementation planning context, see `plans/nsigner_integration_plan.md`. + +## 1) Signer lifecycle and core verbs (exact signatures) + +From `nostr_core/nostr_signer.h`: + +```c +typedef struct nostr_signer nostr_signer_t; + +/* Lifecycle */ +nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]); +void nostr_signer_free(nostr_signer_t* signer); + +/* Core verbs */ +int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]); +int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out); + +/* Encryption verbs (for parity with upcoming remote backends) */ +int nostr_signer_nip04_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out); +int nostr_signer_nip04_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out); +int nostr_signer_nip44_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out); +int nostr_signer_nip44_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out); +``` + +## 2) Remote signer factories + transport API (exact signatures) + +### 2.1 Signer-side remote factories (`nostr_core/nostr_signer.h`) + +```c +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) +nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms); +/* TCP mode requires auth envelope per n_signer; set this before making calls. */ +int nostr_signer_nsigner_set_auth(nostr_signer_t* signer, + const unsigned char auth_privkey[32], + const char* label); +#endif +``` + +### 2.2 Transport constructors + framed I/O + discovery (`nostr_core/nsigner_transport.h`) + +```c +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) +typedef struct nsigner_transport nsigner_transport_t; + +struct nsigner_transport { + int (*send_framed)(nsigner_transport_t* t, const char* json, size_t len); + int (*recv_framed)(nsigner_transport_t* t, char** out_json, size_t* out_len); /* caller frees *out_json */ + void (*close)(nsigner_transport_t* t); + void* ctx; +}; + +/* Raw framed I/O helpers for existing connected file descriptors. */ +int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len); +int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len); + +/* UNIX abstract socket transport (socket_name without leading '@'). */ +nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms); + +/* TCP transport (host + port), with IPv4/IPv6 resolution via getaddrinfo. */ +nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms); + +/* USB CDC-ACM serial transport (device path like /dev/ttyACM0). */ +nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms); + +/* + * Framed transport over existing file descriptors (read_fd, write_fd). + * close() closes both owned fds. If passing STDIN_FILENO/STDOUT_FILENO and you do not + * want them closed, pass dup()'d descriptors instead. + */ +nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms); + +/* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */ +int nsigner_transport_list_unix(char names[][64], int max_names); + +/* Best-effort enumerate serial devices (e.g. /dev/ttyACM*), returns count copied. */ +int nsigner_transport_list_serial(char paths[][64], int max_paths); +#endif +``` + +## 3) Client API + auth contract + +From `nostr_core/nsigner_client.h`: + +```c +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) +typedef struct nsigner_client nsigner_client_t; + +nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport); /* takes ownership of transport */ +void nsigner_client_free(nsigner_client_t* client); + +int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label); + +int nsigner_client_call(nsigner_client_t* client, + const char* method, + cJSON* params, + cJSON** out_result); + +const char* nsigner_client_last_error(const nsigner_client_t* client); +int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]); +#endif +``` + +### Auth and framing notes + +- Framing is 4-byte big-endian payload length + JSON payload. +- Valid payload lengths are `1..65536` bytes (`NSIGNER_CLIENT_MAX_FRAME` in transport implementation). +- TCP requires auth (`nostr_signer_nsigner_set_auth`), unix auth is optional. +- Auth envelope is a kind-27235 event containing tags: + - `nsigner_rpc` + - `nsigner_method` + - `nsigner_body_hash` +- `nsigner_body_hash` is SHA-256 of compact params JSON, or SHA-256 of literal `"null"` when params are null (`nsigner_client_compute_body_hash_hex`). + +## 4) Signer-aware higher-level APIs (`*_with_signer`) + +Below is the full list currently present in headers. + +### NIP-01 (`nostr_core/nip001.h`) + +```c +cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp); +``` + +### NIP-17 (`nostr_core/nip017.h`) + +```c +cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, + int num_relays, + nostr_signer_t* signer); +int nostr_nip17_send_dm_with_signer(cJSON* dm_event, + const char** recipient_pubkeys, + int num_recipients, + nostr_signer_t* signer, + cJSON** gift_wraps_out, + int max_gift_wraps, + long max_delay_sec); +cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap, + nostr_signer_t* signer); +``` + +### NIP-42 (`nostr_core/nip042.h`) + +```c +cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge, + const char* relay_url, + nostr_signer_t* signer, + time_t timestamp); +``` + +### NIP-46 (`nostr_core/nip046.h`) + +```c +cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp); +cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp); +int nostr_nip46_decrypt_event_with_signer(cJSON* event, + nostr_signer_t* signer, + char** output_out); +int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session, + nostr_signer_t* signer, + const char* bunker_url); +``` + +### NIP-60 (`nostr_core/nip060.h`) + +```c +cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs, + const char** deleted_event_ids, + int deleted_count, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id, + const char* mint_url, + time_t expiration, + nostr_signer_t* signer, + time_t timestamp); +``` + +### NIP-61 (`nostr_core/nip061.h`) + +```c +cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data, + nostr_signer_t* signer, + time_t timestamp); +cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id, + const char* nutzap_relay_hint, + const char* sender_pubkey, + const char* created_token_event_id, + const char* created_token_relay_hint, + uint64_t amount, + nostr_signer_t* signer, + time_t timestamp); +``` + +### NIP-59 (`nostr_core/nip059.h`) (additional signer coverage present) + +```c +cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer, + const unsigned char* recipient_public_key, long max_delay_sec); +cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer); +cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key, + nostr_signer_t* signer); +``` + +### Blossom client (`nostr_core/blossom_client.h`) + +```c +char* blossom_create_auth_header_with_signer(nostr_signer_t* signer, + const char* operation, + const char* sha256_hex, + int expiration_seconds, + time_t timestamp); +int blossom_upload_with_signer(const char* server_url, + const unsigned char* data, + size_t data_len, + const char* content_type, + nostr_signer_t* signer, + const char* sha256_hex, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out); +int blossom_upload_file_with_signer(const char* server_url, + const char* file_path, + const char* content_type, + nostr_signer_t* signer, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out); +int blossom_delete_with_signer(const char* server_url, + const char* sha256_hex, + nostr_signer_t* signer, + int timeout_seconds); +``` + +### Relay pool signer entry point (`nostr_core/nostr_core.h`) + +```c +int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable); +``` + +## 5) Feasibility boundaries + +Remotable through `nostr_signer_t` / nsigner client contract: + +- Public key retrieval +- Event signing +- NIP-04 encrypt/decrypt +- NIP-44 encrypt/decrypt + +Not generally remotable without protocol/API redesign: + +- Operations that require direct access to raw secret material for non-signer-verb cryptography. +- In this codebase, Cashu mint-protocol-oriented pieces (e.g. in `cashu_mint`) are the identified example. + +## 6) Minimal C usage snippets + +### (a) Local signer create + sign + publish + +```c +#include "nostr_core/nostr_core.h" + +int main(void) { + unsigned char privkey[32]; + nostr_signer_t* signer; + cJSON* evt; + const char* relays[] = {"wss://relay.laantungir.net"}; + int ok_count = 0; + + if (nostr_init() != NOSTR_SUCCESS) return 1; + + /* fill privkey via your key-management path */ + signer = nostr_signer_local(privkey); + if (!signer) return 1; + + evt = nostr_create_and_sign_event_with_signer(1, "hello", NULL, signer, time(NULL)); + if (!evt) return 1; + + publish_result_t* r = synchronous_publish_event_with_progress( + relays, 1, evt, &ok_count, 10, NULL, NULL, 0, NULL); + + free(r); + cJSON_Delete(evt); + nostr_signer_free(signer); + nostr_cleanup(); + return 0; +} +``` + +### (b) Remote unix signer create + sign + +```c +#include "nostr_core/nostr_core.h" + +int main(void) { + nostr_signer_t* signer; + cJSON* evt; + + if (nostr_init() != NOSTR_SUCCESS) return 1; + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + signer = nostr_signer_nsigner_unix("nsigner-main", NULL, 15000); + if (!signer) return 1; + + evt = nostr_create_and_sign_event_with_signer(1, "signed remotely", NULL, signer, time(NULL)); + if (!evt) return 1; + + cJSON_Delete(evt); + nostr_signer_free(signer); +#endif + + nostr_cleanup(); + return 0; +} +``` + +### (c) TCP signer + auth setup + +```c +#include "nostr_core/nostr_core.h" + +int main(void) { + nostr_signer_t* signer; + unsigned char caller_auth_key[32]; + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + signer = nostr_signer_nsigner_tcp("127.0.0.1", 7777, NULL, 15000); + if (!signer) return 1; + + /* Required for TCP mode */ + if (nostr_signer_nsigner_set_auth(signer, caller_auth_key, "my-caller") != NOSTR_SUCCESS) { + nostr_signer_free(signer); + return 1; + } + + nostr_signer_free(signer); +#endif + + return 0; +} +``` + +## 7) Build/flag wiring summary (exact current lines) + +### `build.sh` + +```bash +SOURCES="$SOURCES nostr_core/nsigner_transport.c" +SOURCES="$SOURCES nostr_core/nsigner_client.c" +CFLAGS="$CFLAGS -DNOSTR_ENABLE_NSIGNER_CLIENT=1" +``` + +### `CMakeLists.txt` + +Desktop branch (`else()`): + +```cmake +add_library(nostr_core STATIC + nostr_core/nostr_common.c + nostr_core/nostr_log.c + nostr_core/request_validator.c + nostr_core/nip001.c + nostr_core/nip006.c + nostr_core/nip019.c + nostr_core/nostr_signer.c + nostr_core/nsigner_transport.c + nostr_core/nsigner_client.c + nostr_core/utils.c + nostr_core/crypto/nostr_secp256k1.c + nostr_core/crypto/nostr_aes.c + nostr_core/crypto/nostr_chacha20.c + cjson/cJSON.c + platform/linux.c +) + +target_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1) +``` + +ESP32 branch (`if(ESP_PLATFORM)`): + +- Does **not** include `nostr_core/nsigner_transport.c` or `nostr_core/nsigner_client.c` in `idf_component_register(SRCS ...)`. +- Sets only: + +```cmake +target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1) +``` + +## 8) Testing pointers + +Relevant tests: + +- `tests/nsigner_client_test.c` +- `tests/signer_modules_test.c` +- `tests/nip01_test.c` (includes local-signer equivalence checks) + +Build tests: + +```bash +./build.sh -t +``` + +Run target binaries: + +```bash +./tests/nsigner_client_test +./tests/signer_modules_test +./tests/nip01_test +``` + +## 9) Driver app signer modes (`examples/note_poster.c`) + +Supported `--signer` values in the driver app: + +- `local` (default) +- `unix:` +- `serial:` +- `tcp::` +- `fds::` + +Examples: + +```bash +./examples/note_poster +./examples/note_poster --signer unix: +./examples/note_poster --signer serial:/dev/ttyACM0 +./examples/note_poster --signer tcp:: +./examples/note_poster --signer fds:: +``` + +Local mode warning (from app behavior): this is test-key-only usage; do not use production keys. diff --git a/nostr_core/blossom_client.c b/nostr_core/blossom_client.c index ca76180d..85164a05 100644 --- a/nostr_core/blossom_client.c +++ b/nostr_core/blossom_client.c @@ -79,11 +79,12 @@ void blossom_set_ca_bundle(const char* ca_bundle_path) { nostr_http_set_ca_bundle(ca_bundle_path); } -char* blossom_create_auth_header(const unsigned char* private_key, - const char* operation, - const char* sha256_hex, - int expiration_seconds) { - if (!private_key || !operation || operation[0] == '\0') return NULL; +char* blossom_create_auth_header_with_signer(nostr_signer_t* signer, + const char* operation, + const char* sha256_hex, + int expiration_seconds, + time_t timestamp) { + if (!signer || !operation || operation[0] == '\0') return NULL; if (sha256_hex && !is_hex64_local(sha256_hex)) return NULL; cJSON* tags = cJSON_CreateArray(); @@ -101,8 +102,9 @@ char* blossom_create_auth_header(const unsigned char* private_key, cJSON_AddItemToArray(tags, x_tag); } + time_t now = (timestamp > 0) ? timestamp : time(NULL); int exp = expiration_seconds > 0 ? expiration_seconds : 300; - long until = (long)time(NULL) + exp; + long until = (long)now + exp; char exp_buf[32]; snprintf(exp_buf, sizeof(exp_buf), "%ld", until); cJSON* e_tag = cJSON_CreateArray(); @@ -110,7 +112,7 @@ char* blossom_create_auth_header(const unsigned char* private_key, cJSON_AddItemToArray(e_tag, cJSON_CreateString(exp_buf)); cJSON_AddItemToArray(tags, e_tag); - cJSON* evt = nostr_create_and_sign_event(24242, "", tags, private_key, time(NULL)); + cJSON* evt = nostr_create_and_sign_event_with_signer(24242, "", tags, signer, now); cJSON_Delete(tags); if (!evt) return NULL; @@ -144,14 +146,80 @@ char* blossom_create_auth_header(const unsigned char* private_key, return header; } -int blossom_upload(const char* server_url, - const unsigned char* data, - size_t data_len, - const char* content_type, - const unsigned char* private_key, - const char* sha256_hex, - int timeout_seconds, - blossom_blob_descriptor_t* descriptor_out) { +char* blossom_create_auth_header(const unsigned char* private_key, + const char* operation, + const char* sha256_hex, + int expiration_seconds) { + if (!private_key || !operation || operation[0] == '\0') return NULL; + if (sha256_hex && !is_hex64_local(sha256_hex)) return NULL; + + cJSON* tags = cJSON_CreateArray(); + if (!tags) return NULL; + + cJSON* t_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(t_tag, cJSON_CreateString("t")); + cJSON_AddItemToArray(t_tag, cJSON_CreateString(operation)); + cJSON_AddItemToArray(tags, t_tag); + + if (sha256_hex) { + cJSON* x_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(x_tag, cJSON_CreateString("x")); + cJSON_AddItemToArray(x_tag, cJSON_CreateString(sha256_hex)); + cJSON_AddItemToArray(tags, x_tag); + } + + int exp = expiration_seconds > 0 ? expiration_seconds : 300; + long now = (long)time(NULL); + long until = now + exp; + char exp_buf[32]; + snprintf(exp_buf, sizeof(exp_buf), "%ld", until); + cJSON* e_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(e_tag, cJSON_CreateString("expiration")); + cJSON_AddItemToArray(e_tag, cJSON_CreateString(exp_buf)); + cJSON_AddItemToArray(tags, e_tag); + + cJSON* evt = nostr_create_and_sign_event(24242, "", tags, private_key, (time_t)now); + cJSON_Delete(tags); + if (!evt) return NULL; + + char* evt_json = cJSON_PrintUnformatted(evt); + cJSON_Delete(evt); + if (!evt_json) return NULL; + + size_t evt_len = strlen(evt_json); + size_t b64_cap = ((evt_len + 2U) / 3U) * 4U + 8U; + char* b64 = (char*)malloc(b64_cap); + if (!b64) { + free(evt_json); + return NULL; + } + + size_t n = base64_encode((const unsigned char*)evt_json, evt_len, b64, b64_cap); + free(evt_json); + if (n == 0) { + free(b64); + return NULL; + } + + size_t hdr_cap = n + 16U; + char* header = (char*)malloc(hdr_cap); + if (!header) { + free(b64); + return NULL; + } + snprintf(header, hdr_cap, "Nostr %s", b64); + free(b64); + return header; +} + +int blossom_upload_with_signer(const char* server_url, + const unsigned char* data, + size_t data_len, + const char* content_type, + nostr_signer_t* signer, + const char* sha256_hex, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out) { if (!server_url || !data || data_len == 0 || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT; char hash_hex[65] = {0}; @@ -180,8 +248,8 @@ int blossom_upload(const char* server_url, } char* auth = NULL; - if (private_key) { - auth = blossom_create_auth_header(private_key, "upload", hash_hex, 300); + if (signer) { + auth = blossom_create_auth_header_with_signer(signer, "upload", hash_hex, 300, 0); if (auth) { snprintf(auth_buf, sizeof(auth_buf), "Authorization: %s", auth); headers[h++] = auth_buf; @@ -224,12 +292,33 @@ int blossom_upload(const char* server_url, return NOSTR_SUCCESS; } -int blossom_upload_file(const char* server_url, - const char* file_path, - const char* content_type, - const unsigned char* private_key, - int timeout_seconds, - blossom_blob_descriptor_t* descriptor_out) { +int blossom_upload(const char* server_url, + const unsigned char* data, + size_t data_len, + const char* content_type, + const unsigned char* private_key, + const char* sha256_hex, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out) { + nostr_signer_t* signer = NULL; + int rc; + + if (private_key) { + signer = nostr_signer_local(private_key); + } + + rc = blossom_upload_with_signer(server_url, data, data_len, content_type, + signer, sha256_hex, timeout_seconds, descriptor_out); + nostr_signer_free(signer); + return rc; +} + +int blossom_upload_file_with_signer(const char* server_url, + const char* file_path, + const char* content_type, + nostr_signer_t* signer, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out) { if (!server_url || !file_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT; FILE* fp = fopen(file_path, "rb"); @@ -261,11 +350,30 @@ int blossom_upload_file(const char* server_url, return NOSTR_ERROR_IO_FAILED; } - int rc = blossom_upload(server_url, buf, n, content_type, private_key, NULL, timeout_seconds, descriptor_out); + int rc = blossom_upload_with_signer(server_url, buf, n, content_type, signer, NULL, timeout_seconds, descriptor_out); free(buf); return rc; } +int blossom_upload_file(const char* server_url, + const char* file_path, + const char* content_type, + const unsigned char* private_key, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out) { + nostr_signer_t* signer = NULL; + int rc; + + if (private_key) { + signer = nostr_signer_local(private_key); + } + + rc = blossom_upload_file_with_signer(server_url, file_path, content_type, + signer, timeout_seconds, descriptor_out); + nostr_signer_free(signer); + return rc; +} + int blossom_download(const char* server_url, const char* sha256_hex, int timeout_seconds, @@ -415,18 +523,18 @@ int blossom_head(const char* server_url, return NOSTR_SUCCESS; } -int blossom_delete(const char* server_url, - const char* sha256_hex, - const unsigned char* private_key, - int timeout_seconds) { - if (!server_url || !sha256_hex || !private_key || !is_hex64_local(sha256_hex)) { +int blossom_delete_with_signer(const char* server_url, + const char* sha256_hex, + nostr_signer_t* signer, + int timeout_seconds) { + if (!server_url || !sha256_hex || !signer || !is_hex64_local(sha256_hex)) { return NOSTR_ERROR_INVALID_INPUT; } char url[1024]; if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT; - char* auth = blossom_create_auth_header(private_key, "delete", sha256_hex, 300); + char* auth = blossom_create_auth_header_with_signer(signer, "delete", sha256_hex, 300, 0); if (!auth) return NOSTR_ERROR_CRYPTO_FAILED; char auth_header[1200]; @@ -450,6 +558,27 @@ int blossom_delete(const char* server_url, return ok; } +int blossom_delete(const char* server_url, + const char* sha256_hex, + const unsigned char* private_key, + int timeout_seconds) { + nostr_signer_t* signer; + int rc; + + if (!private_key) { + return NOSTR_ERROR_INVALID_INPUT; + } + + signer = nostr_signer_local(private_key); + if (!signer) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + rc = blossom_delete_with_signer(server_url, sha256_hex, signer, timeout_seconds); + nostr_signer_free(signer); + return rc; +} + int blossom_list(const char* server_url, const char* pubkey_hex, int timeout_seconds, diff --git a/nostr_core/blossom_client.h b/nostr_core/blossom_client.h index 8f6d97a7..14b3b14d 100644 --- a/nostr_core/blossom_client.h +++ b/nostr_core/blossom_client.h @@ -6,7 +6,9 @@ #define NOSTR_BLOSSOM_CLIENT_H #include "nostr_common.h" +#include "nostr_signer.h" #include +#include #ifdef __cplusplus extern "C" { @@ -27,6 +29,12 @@ char* blossom_create_auth_header(const unsigned char* private_key, const char* sha256_hex, int expiration_seconds); +char* blossom_create_auth_header_with_signer(nostr_signer_t* signer, + const char* operation, + const char* sha256_hex, + int expiration_seconds, + time_t timestamp); + int blossom_upload(const char* server_url, const unsigned char* data, size_t data_len, @@ -36,6 +44,15 @@ int blossom_upload(const char* server_url, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out); +int blossom_upload_with_signer(const char* server_url, + const unsigned char* data, + size_t data_len, + const char* content_type, + nostr_signer_t* signer, + const char* sha256_hex, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out); + int blossom_upload_file(const char* server_url, const char* file_path, const char* content_type, @@ -43,6 +60,13 @@ int blossom_upload_file(const char* server_url, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out); +int blossom_upload_file_with_signer(const char* server_url, + const char* file_path, + const char* content_type, + nostr_signer_t* signer, + int timeout_seconds, + blossom_blob_descriptor_t* descriptor_out); + int blossom_download(const char* server_url, const char* sha256_hex, int timeout_seconds, @@ -69,6 +93,11 @@ int blossom_delete(const char* server_url, const unsigned char* private_key, int timeout_seconds); +int blossom_delete_with_signer(const char* server_url, + const char* sha256_hex, + nostr_signer_t* signer, + int timeout_seconds); + int blossom_list(const char* server_url, const char* pubkey_hex, int timeout_seconds, diff --git a/nostr_core/core_relay_pool.c b/nostr_core/core_relay_pool.c index 53edc184..77567d0e 100644 --- a/nostr_core/core_relay_pool.c +++ b/nostr_core/core_relay_pool.c @@ -208,8 +208,8 @@ struct nostr_relay_pool { // NIP-42 Authentication configuration int nip42_enabled; - unsigned char private_key[32]; - int has_private_key; + nostr_signer_t* auth_signer; + int owns_auth_signer; }; // Helper function to generate unique subscription IDs @@ -664,23 +664,21 @@ nostr_relay_pool_t* nostr_relay_pool_create_compat(void) { return nostr_relay_pool_create(NULL); } -int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, - const unsigned char* private_key, - int enable) { +int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, + nostr_signer_t* signer, + int enable) { if (!pool) { return NOSTR_ERROR_INVALID_INPUT; } - pool->nip42_enabled = enable ? 1 : 0; - - if (private_key) { - memcpy(pool->private_key, private_key, sizeof(pool->private_key)); - pool->has_private_key = 1; - } else { - memset(pool->private_key, 0, sizeof(pool->private_key)); - pool->has_private_key = 0; + if (pool->owns_auth_signer && pool->auth_signer) { + nostr_signer_free(pool->auth_signer); } + pool->nip42_enabled = enable ? 1 : 0; + pool->auth_signer = pool->nip42_enabled ? signer : NULL; + pool->owns_auth_signer = 0; + // Propagate to existing relay connections for (int i = 0; i < pool->relay_count; i++) { if (pool->relays[i]) { @@ -696,6 +694,36 @@ int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, return NOSTR_SUCCESS; } +int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, + const unsigned char* private_key, + int enable) { + nostr_signer_t* signer = NULL; + int rc; + + if (!pool) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (enable && private_key) { + signer = nostr_signer_local(private_key); + if (!signer) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + } + + rc = nostr_relay_pool_set_auth_with_signer(pool, signer, enable); + if (rc != NOSTR_SUCCESS) { + nostr_signer_free(signer); + return rc; + } + + if (signer) { + pool->owns_auth_signer = 1; + } + + return NOSTR_SUCCESS; +} + int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) { if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) { return NOSTR_ERROR_INVALID_INPUT; @@ -816,9 +844,10 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) { } } - if (pool->has_private_key) { - memset(pool->private_key, 0, sizeof(pool->private_key)); - pool->has_private_key = 0; + if (pool->owns_auth_signer && pool->auth_signer) { + nostr_signer_free(pool->auth_signer); + pool->auth_signer = NULL; + pool->owns_auth_signer = 0; } free(pool); @@ -1218,11 +1247,11 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* relay->auth_state = NOSTR_AUTH_STATE_REJECTED; // If we already have a challenge, immediately attempt re-authentication - if (relay->nip42_enabled && pool->has_private_key && relay->auth_challenge[0] != '\0') { - cJSON* auth_event = nostr_nip42_create_auth_event( + if (relay->nip42_enabled && pool->auth_signer && relay->auth_challenge[0] != '\0') { + cJSON* auth_event = nostr_nip42_create_auth_event_with_signer( relay->auth_challenge, relay->url, - pool->private_key, + pool->auth_signer, 0 ); if (auth_event) { @@ -1265,7 +1294,7 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* } } else if (strcmp(msg_type, "AUTH") == 0) { // Handle AUTH challenge message: ["AUTH", ] - if (relay->nip42_enabled && pool->has_private_key && + if (relay->nip42_enabled && pool->auth_signer && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1); if (cJSON_IsString(challenge_json)) { @@ -1276,10 +1305,10 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* relay->auth_challenge_time = time(NULL); relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED; - cJSON* auth_event = nostr_nip42_create_auth_event( + cJSON* auth_event = nostr_nip42_create_auth_event_with_signer( challenge, relay->url, - pool->private_key, + pool->auth_signer, 0 ); if (auth_event) { @@ -1349,28 +1378,72 @@ cJSON** nostr_relay_pool_query_sync( int connected_count = 0; for (int i = 0; i < relay_count && connected_count < NOSTR_POOL_MAX_RELAYS; i++) { - relay_connection_t* relay = find_relay_by_url(pool, relay_urls[i]); + relay_connection_t* relay = NULL; + int ensure_rc = -1; + + query_sync_debug_log("connect_step_start idx=%d/%d relay=%s connected_so_far=%d", + i + 1, + relay_count, + relay_urls[i] ? relay_urls[i] : "(null)", + connected_count); + + relay = find_relay_by_url(pool, relay_urls[i]); + query_sync_debug_log("find_relay idx=%d relay=%s found=%s", + i + 1, + relay_urls[i] ? relay_urls[i] : "(null)", + relay ? "yes" : "no"); + if (!relay) { - // Add relay if it doesn't exist - if (nostr_relay_pool_add_relay(pool, relay_urls[i]) == NOSTR_SUCCESS) { + int add_rc = nostr_relay_pool_add_relay(pool, relay_urls[i]); + query_sync_debug_log("add_relay idx=%d relay=%s rc=%d", + i + 1, + relay_urls[i] ? relay_urls[i] : "(null)", + add_rc); + if (add_rc == NOSTR_SUCCESS) { relay = find_relay_by_url(pool, relay_urls[i]); + query_sync_debug_log("find_relay_after_add idx=%d relay=%s found=%s", + i + 1, + relay_urls[i] ? relay_urls[i] : "(null)", + relay ? "yes" : "no"); } } - - if (relay && ensure_relay_connection(relay) == 0) { + + if (!relay) { + query_sync_debug_log("connect_step_skip idx=%d relay=%s reason=no_relay_object", + i + 1, + relay_urls[i] ? relay_urls[i] : "(null)"); + continue; + } + + query_sync_debug_log("ensure_connection_begin idx=%d relay=%s", i + 1, relay->url); + ensure_rc = ensure_relay_connection(relay); + query_sync_debug_log("ensure_connection_end idx=%d relay=%s rc=%d ws_client=%s state=%d", + i + 1, + relay->url, + ensure_rc, + relay->ws_client ? "set" : "null", + (int)relay->status); + + if (ensure_rc == 0) { connected_relays[connected_count++] = relay; - + // Add subscription timing for latency measurement add_subscription_timing(relay, subscription_id); - + // Send REQ message int send_rc = nostr_relay_send_req(relay->ws_client, subscription_id, filter); - query_sync_debug_log("send_req relay=%s sub_id=%s rc=%d", relay->url, subscription_id, send_rc); + query_sync_debug_log("send_req relay=%s sub_id=%s rc=%d connected_count=%d", relay->url, subscription_id, send_rc, connected_count); if (send_rc < 0) { // Remove timing if send failed remove_subscription_timing(relay, subscription_id); connected_count--; // Don't count failed connections + query_sync_debug_log("send_req_failed relay=%s sub_id=%s connected_count=%d", relay->url, subscription_id, connected_count); } + } else { + query_sync_debug_log("connect_step_skip idx=%d relay=%s reason=ensure_failed rc=%d", + i + 1, + relay->url, + ensure_rc); } } @@ -1388,11 +1461,17 @@ cJSON** nostr_relay_pool_query_sync( query_sync_debug_log("query_loop_start sub_id=%s timeout_ms=%d timeout_seconds=%d connected_relays=%d", subscription_id, timeout_ms, timeout_seconds, connected_count); while (time(NULL) - start_time < timeout_seconds && eose_count < connected_count) { + query_sync_debug_log("query_loop_tick sub_id=%s elapsed_s=%d timeout_s=%d eose=%d/%d", + subscription_id, + (int)(time(NULL) - start_time), + timeout_seconds, + eose_count, + connected_count); for (int i = 0; i < connected_count; i++) { relay_connection_t* relay = connected_relays[i]; - char buffer[65536]; - int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100); + char buffer[262144]; + int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 1000); query_sync_debug_log("receive relay=%s len=%d", relay->url, len); if (len > 0) { buffer[len] = '\0'; @@ -1486,7 +1565,7 @@ cJSON** nostr_relay_pool_query_sync( } } } else if (msg_type && strcmp(msg_type, "AUTH") == 0) { - if (relay->nip42_enabled && pool->has_private_key && + if (relay->nip42_enabled && pool->auth_signer && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1); if (cJSON_IsString(challenge_json)) { @@ -1498,10 +1577,10 @@ cJSON** nostr_relay_pool_query_sync( relay->auth_challenge_time = time(NULL); relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED; - cJSON* auth_event = nostr_nip42_create_auth_event( + cJSON* auth_event = nostr_nip42_create_auth_event_with_signer( challenge, relay->url, - pool->private_key, + pool->auth_signer, 0 ); if (auth_event) { diff --git a/nostr_core/crypto/nostr_chacha20poly1305.c b/nostr_core/crypto/nostr_chacha20poly1305.c new file mode 100644 index 00000000..95516b2c --- /dev/null +++ b/nostr_core/crypto/nostr_chacha20poly1305.c @@ -0,0 +1,192 @@ +/* + * nostr_chacha20poly1305.c - ChaCha20-Poly1305 AEAD implementation + * + * RFC 8439 Section 2.8 + */ + +#include +#include +#include +#include + +// ChaCha20 private API (provided by nostr_chacha20.c) +int chacha20_block(const uint8_t key[32], uint32_t counter, + const uint8_t nonce[12], uint8_t output[64]); +int chacha20_encrypt(const uint8_t key[32], uint32_t counter, + const uint8_t nonce[12], const uint8_t* input, + uint8_t* output, size_t length); + +// Poly1305 API (provided by nostr_poly1305.c) +int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg, + size_t msg_len, unsigned char tag[16]); + +static void store64_le(unsigned char out[8], uint64_t v) { + out[0] = (unsigned char)(v & 0xff); + out[1] = (unsigned char)((v >> 8) & 0xff); + out[2] = (unsigned char)((v >> 16) & 0xff); + out[3] = (unsigned char)((v >> 24) & 0xff); + out[4] = (unsigned char)((v >> 32) & 0xff); + out[5] = (unsigned char)((v >> 40) & 0xff); + out[6] = (unsigned char)((v >> 48) & 0xff); + out[7] = (unsigned char)((v >> 56) & 0xff); +} + +static int constant_time_tag_eq(const unsigned char a[16], const unsigned char b[16]) { + unsigned char diff = 0; + size_t i; + for (i = 0; i < 16; i++) { + diff |= (unsigned char)(a[i] ^ b[i]); + } + return diff == 0; +} + +static int build_poly1305_input(const unsigned char *aad, size_t aad_len, + const unsigned char *ciphertext, size_t ct_len, + unsigned char **out, size_t *out_len) { + size_t aad_pad = (16 - (aad_len % 16)) % 16; + size_t ct_pad = (16 - (ct_len % 16)) % 16; + size_t total = aad_len + aad_pad + ct_len + ct_pad + 16; // len(aad)||len(ct) + unsigned char *buf; + size_t pos = 0; + unsigned char len_block[16]; + + if (!out || !out_len) { + return -1; + } + + buf = (unsigned char *)malloc(total); + if (!buf) { + return -1; + } + + if (aad_len > 0 && aad) { + memcpy(buf + pos, aad, aad_len); + pos += aad_len; + } + if (aad_pad > 0) { + memset(buf + pos, 0, aad_pad); + pos += aad_pad; + } + + if (ct_len > 0 && ciphertext) { + memcpy(buf + pos, ciphertext, ct_len); + pos += ct_len; + } + if (ct_pad > 0) { + memset(buf + pos, 0, ct_pad); + pos += ct_pad; + } + + store64_le(len_block, (uint64_t)aad_len); + store64_le(len_block + 8, (uint64_t)ct_len); + memcpy(buf + pos, len_block, sizeof(len_block)); + pos += sizeof(len_block); + + *out = buf; + *out_len = pos; + return 0; +} + +int nostr_chacha20poly1305_encrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *plaintext, size_t pt_len, + unsigned char *ciphertext, + unsigned char tag[16]) { + unsigned char block0[64]; + unsigned char poly_key[32]; + unsigned char *poly_in = NULL; + size_t poly_in_len = 0; + int rc; + + if (!key || !nonce || !ciphertext || !tag || (!plaintext && pt_len != 0) || (!aad && aad_len != 0)) { + return -1; + } + + if (chacha20_block(key, 0, nonce, block0) != 0) { + return -1; + } + memcpy(poly_key, block0, sizeof(poly_key)); + + if (pt_len > 0) { + rc = chacha20_encrypt(key, 1, nonce, plaintext, ciphertext, pt_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + return -1; + } + } + + rc = build_poly1305_input(aad, aad_len, ciphertext, pt_len, &poly_in, &poly_in_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + return -1; + } + + rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, tag); + + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + + return rc; +} + +int nostr_chacha20poly1305_decrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *ciphertext, size_t ct_len, + const unsigned char tag[16], + unsigned char *plaintext) { + unsigned char block0[64]; + unsigned char poly_key[32]; + unsigned char expected_tag[16]; + unsigned char *poly_in = NULL; + size_t poly_in_len = 0; + int rc; + + if (!key || !nonce || !tag || !plaintext || (!ciphertext && ct_len != 0) || (!aad && aad_len != 0)) { + return -1; + } + + if (chacha20_block(key, 0, nonce, block0) != 0) { + return -1; + } + memcpy(poly_key, block0, sizeof(poly_key)); + + rc = build_poly1305_input(aad, aad_len, ciphertext, ct_len, &poly_in, &poly_in_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + return -1; + } + + rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, expected_tag); + if (rc != 0 || !constant_time_tag_eq(tag, expected_tag)) { + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(expected_tag, 0, sizeof(expected_tag)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + return -1; + } + + if (ct_len > 0) { + rc = chacha20_encrypt(key, 1, nonce, ciphertext, plaintext, ct_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(expected_tag, 0, sizeof(expected_tag)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + return -1; + } + } + + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(expected_tag, 0, sizeof(expected_tag)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + + return 0; +} diff --git a/nostr_core/crypto/nostr_poly1305.c b/nostr_core/crypto/nostr_poly1305.c new file mode 100644 index 00000000..2fe385c2 --- /dev/null +++ b/nostr_core/crypto/nostr_poly1305.c @@ -0,0 +1,262 @@ +/* + * nostr_poly1305.c - Poly1305 message authentication code + * + * Public-domain style 32-bit implementation based on the poly1305-donna + * approach, adapted for nostr_core_lib naming and conventions. + * + * RFC 8439 Section 2.5 + */ + +#include +#include +#include + +typedef struct { + uint32_t r0, r1, r2, r3, r4; + uint32_t s1, s2, s3, s4; + uint32_t h0, h1, h2, h3, h4; + uint32_t pad0, pad1, pad2, pad3; + size_t leftover; + unsigned char buffer[16]; + unsigned char final; +} nostr_poly1305_ctx_t; + +static uint32_t u8to32_le(const unsigned char *p) { + return ((uint32_t)p[0]) | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void u32to8_le(unsigned char *p, uint32_t v) { + p[0] = (unsigned char)(v & 0xff); + p[1] = (unsigned char)((v >> 8) & 0xff); + p[2] = (unsigned char)((v >> 16) & 0xff); + p[3] = (unsigned char)((v >> 24) & 0xff); +} + +static void poly1305_blocks(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) { + const uint32_t r0 = st->r0; + const uint32_t r1 = st->r1; + const uint32_t r2 = st->r2; + const uint32_t r3 = st->r3; + const uint32_t r4 = st->r4; + const uint32_t s1 = st->s1; + const uint32_t s2 = st->s2; + const uint32_t s3 = st->s3; + const uint32_t s4 = st->s4; + uint32_t h0 = st->h0; + uint32_t h1 = st->h1; + uint32_t h2 = st->h2; + uint32_t h3 = st->h3; + uint32_t h4 = st->h4; + const uint32_t hibit = st->final ? 0 : (1U << 24); + + while (bytes >= 16) { + uint32_t t0 = u8to32_le(m + 0); + uint32_t t1 = u8to32_le(m + 4); + uint32_t t2 = u8to32_le(m + 8); + uint32_t t3 = u8to32_le(m + 12); + + uint64_t d0, d1, d2, d3, d4; + uint32_t c; + + h0 += ( t0 ) & 0x3ffffff; + h1 += (((t1 << 6) | (t0 >> 26)) ) & 0x3ffffff; + h2 += (((t2 << 12) | (t1 >> 20))) & 0x3ffffff; + h3 += (((t3 << 18) | (t2 >> 14))) & 0x3ffffff; + h4 += (( t3 >> 8) ) & 0x00ffffff; + h4 += hibit; + + d0 = ((uint64_t)h0 * r0) + ((uint64_t)h1 * s4) + ((uint64_t)h2 * s3) + ((uint64_t)h3 * s2) + ((uint64_t)h4 * s1); + d1 = ((uint64_t)h0 * r1) + ((uint64_t)h1 * r0) + ((uint64_t)h2 * s4) + ((uint64_t)h3 * s3) + ((uint64_t)h4 * s2); + d2 = ((uint64_t)h0 * r2) + ((uint64_t)h1 * r1) + ((uint64_t)h2 * r0) + ((uint64_t)h3 * s4) + ((uint64_t)h4 * s3); + d3 = ((uint64_t)h0 * r3) + ((uint64_t)h1 * r2) + ((uint64_t)h2 * r1) + ((uint64_t)h3 * r0) + ((uint64_t)h4 * s4); + d4 = ((uint64_t)h0 * r4) + ((uint64_t)h1 * r3) + ((uint64_t)h2 * r2) + ((uint64_t)h3 * r1) + ((uint64_t)h4 * r0); + + c = (uint32_t)(d0 >> 26); h0 = (uint32_t)d0 & 0x3ffffff; + d1 += c; + c = (uint32_t)(d1 >> 26); h1 = (uint32_t)d1 & 0x3ffffff; + d2 += c; + c = (uint32_t)(d2 >> 26); h2 = (uint32_t)d2 & 0x3ffffff; + d3 += c; + c = (uint32_t)(d3 >> 26); h3 = (uint32_t)d3 & 0x3ffffff; + d4 += c; + c = (uint32_t)(d4 >> 26); h4 = (uint32_t)d4 & 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; h0 &= 0x3ffffff; + h1 += c; + + m += 16; + bytes -= 16; + } + + st->h0 = h0; + st->h1 = h1; + st->h2 = h2; + st->h3 = h3; + st->h4 = h4; +} + +void nostr_poly1305_init(nostr_poly1305_ctx_t *st, const unsigned char key[32]) { + uint32_t t0, t1, t2, t3; + + t0 = u8to32_le(key + 0); + t1 = u8to32_le(key + 4); + t2 = u8to32_le(key + 8); + t3 = u8to32_le(key + 12); + + st->r0 = ( t0 ) & 0x3ffffff; + st->r1 = (((t1 << 6) | (t0 >> 26)) ) & 0x3ffff03; + st->r2 = (((t2 << 12) | (t1 >> 20))) & 0x3ffc0ff; + st->r3 = (((t3 << 18) | (t2 >> 14))) & 0x3f03fff; + st->r4 = (( t3 >> 8) ) & 0x00fffff; + + st->s1 = st->r1 * 5; + st->s2 = st->r2 * 5; + st->s3 = st->r3 * 5; + st->s4 = st->r4 * 5; + + st->h0 = 0; + st->h1 = 0; + st->h2 = 0; + st->h3 = 0; + st->h4 = 0; + + st->pad0 = u8to32_le(key + 16); + st->pad1 = u8to32_le(key + 20); + st->pad2 = u8to32_le(key + 24); + st->pad3 = u8to32_le(key + 28); + + st->leftover = 0; + st->final = 0; +} + +void nostr_poly1305_update(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) { + size_t i; + + if (st->leftover) { + size_t want = (size_t)(16 - st->leftover); + if (want > bytes) { + want = bytes; + } + for (i = 0; i < want; i++) { + st->buffer[st->leftover + i] = m[i]; + } + bytes -= want; + m += want; + st->leftover += want; + if (st->leftover < 16) { + return; + } + poly1305_blocks(st, st->buffer, 16); + st->leftover = 0; + } + + if (bytes >= 16) { + size_t want = bytes & ~(size_t)0xf; + poly1305_blocks(st, m, want); + m += want; + bytes -= want; + } + + if (bytes) { + for (i = 0; i < bytes; i++) { + st->buffer[st->leftover + i] = m[i]; + } + st->leftover += bytes; + } +} + +void nostr_poly1305_final(nostr_poly1305_ctx_t *st, unsigned char tag[16]) { + uint32_t h0, h1, h2, h3, h4, c; + uint32_t g0, g1, g2, g3, g4; + uint64_t f; + uint32_t mask; + + if (st->leftover) { + size_t i = st->leftover; + st->buffer[i++] = 1; + for (; i < 16; i++) { + st->buffer[i] = 0; + } + st->final = 1; + poly1305_blocks(st, st->buffer, 16); + } + + h0 = st->h0; + h1 = st->h1; + h2 = st->h2; + h3 = st->h3; + h4 = st->h4; + + c = h1 >> 26; h1 &= 0x3ffffff; + h2 += c; + c = h2 >> 26; h2 &= 0x3ffffff; + h3 += c; + c = h3 >> 26; h3 &= 0x3ffffff; + h4 += c; + c = h4 >> 26; h4 &= 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; h0 &= 0x3ffffff; + h1 += c; + + g0 = h0 + 5; + c = g0 >> 26; g0 &= 0x3ffffff; + g1 = h1 + c; + c = g1 >> 26; g1 &= 0x3ffffff; + g2 = h2 + c; + c = g2 >> 26; g2 &= 0x3ffffff; + g3 = h3 + c; + c = g3 >> 26; g3 &= 0x3ffffff; + g4 = h4 + c - (1U << 26); + + mask = (g4 >> 31) - 1U; + g0 &= mask; + g1 &= mask; + g2 &= mask; + g3 &= mask; + g4 &= mask; + mask = ~mask; + h0 = (h0 & mask) | g0; + h1 = (h1 & mask) | g1; + h2 = (h2 & mask) | g2; + h3 = (h3 & mask) | g3; + h4 = (h4 & mask) | g4; + + h0 = ((h0 ) | (h1 << 26)) & 0xffffffff; + h1 = ((h1 >> 6 ) | (h2 << 20)) & 0xffffffff; + h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; + h3 = ((h3 >> 18) | (h4 << 8 )) & 0xffffffff; + + f = (uint64_t)h0 + st->pad0; + h0 = (uint32_t)f; + f = (uint64_t)h1 + st->pad1 + (f >> 32); + h1 = (uint32_t)f; + f = (uint64_t)h2 + st->pad2 + (f >> 32); + h2 = (uint32_t)f; + f = (uint64_t)h3 + st->pad3 + (f >> 32); + h3 = (uint32_t)f; + + u32to8_le(tag + 0, h0); + u32to8_le(tag + 4, h1); + u32to8_le(tag + 8, h2); + u32to8_le(tag + 12, h3); + + memset(st, 0, sizeof(*st)); +} + +int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg, + size_t msg_len, unsigned char tag[16]) { + nostr_poly1305_ctx_t ctx; + + if (!key || (!msg && msg_len != 0) || !tag) { + return -1; + } + + nostr_poly1305_init(&ctx, key); + nostr_poly1305_update(&ctx, msg, msg_len); + nostr_poly1305_final(&ctx, tag); + + return 0; +} diff --git a/nostr_core/crypto/nostr_secp256k1.c b/nostr_core/crypto/nostr_secp256k1.c index 7ac9a4fa..eebd28bc 100644 --- a/nostr_core/crypto/nostr_secp256k1.c +++ b/nostr_core/crypto/nostr_secp256k1.c @@ -3,9 +3,8 @@ #include #include #include -#include -#include #include +#include "../nostr_platform.h" /* * PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED @@ -313,23 +312,9 @@ int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) { return 0; } - // Try to use /dev/urandom for good randomness - int fd = open("/dev/urandom", O_RDONLY); - if (fd >= 0) { - ssize_t result = read(fd, buf, len); - close(fd); - if (result == (ssize_t)len) { - return 1; - } + if (nostr_platform_random(buf, len) != 0) { + return 0; } - - // Fallback to a simple PRNG (not cryptographically secure, but better than nothing) - // In a real implementation, you'd want to use a proper CSPRNG - static unsigned long seed = 1; - for (size_t i = 0; i < len; i++) { - seed = seed * 1103515245 + 12345; - buf[i] = (unsigned char)(seed >> 16); - } - + return 1; } diff --git a/nostr_core/nip001.c b/nostr_core/nip001.c index 69aa77f2..e2ca5d04 100644 --- a/nostr_core/nip001.c +++ b/nostr_core/nip001.c @@ -7,6 +7,7 @@ #include "nip001.h" #include "utils.h" #include "../cjson/cJSON.h" +#include "nostr_signer.h" #include #include #include @@ -32,116 +33,73 @@ int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, u /** * Create and sign a NOSTR event */ -cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) { - if (!private_key) { +cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp) { + cJSON* event = NULL; + cJSON* signed_event = NULL; + char pubkey_hex[65]; + int rc; + + if (!signer) { return NULL; } - + if (!content) { content = ""; // Default to empty content } - - // Convert private key to public key - unsigned char public_key[32]; - if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) { + + rc = nostr_signer_get_public_key(signer, pubkey_hex); + if (rc != NOSTR_SUCCESS) { return NULL; } - - // Convert public key to hex - char pubkey_hex[65]; - nostr_bytes_to_hex(public_key, 32, pubkey_hex); - + // Create event structure - cJSON* event = cJSON_CreateObject(); + event = cJSON_CreateObject(); if (!event) { return NULL; } - + // Use provided timestamp or current time if timestamp is 0 time_t event_time = (timestamp == 0) ? time(NULL) : timestamp; - + cJSON_AddStringToObject(event, "pubkey", pubkey_hex); cJSON_AddNumberToObject(event, "created_at", (double)event_time); cJSON_AddNumberToObject(event, "kind", kind); - + // Add tags (copy provided tags or create empty array) if (tags) { cJSON_AddItemToObject(event, "tags", cJSON_Duplicate(tags, 1)); } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } - + cJSON_AddStringToObject(event, "content", content); - - // ============================================================================ - // INLINE SERIALIZATION AND SIGNING LOGIC - // ============================================================================ - - // Get event fields for serialization - cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey"); - cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at"); - cJSON* kind_item = cJSON_GetObjectItem(event, "kind"); - cJSON* tags_item = cJSON_GetObjectItem(event, "tags"); - cJSON* content_item = cJSON_GetObjectItem(event, "content"); - - if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) { - cJSON_Delete(event); + + rc = nostr_signer_sign_event(signer, event, &signed_event); + cJSON_Delete(event); + + if (rc != NOSTR_SUCCESS) { return NULL; } - - // Create serialization array: [0, pubkey, created_at, kind, tags, content] - cJSON* serialize_array = cJSON_CreateArray(); - if (!serialize_array) { - cJSON_Delete(event); + + return signed_event; +} + +cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) { + cJSON* out = NULL; + nostr_signer_t* signer = NULL; + + if (!private_key) { return NULL; } - - cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0)); - cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1)); - cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1)); - cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1)); - cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1)); - cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1)); - - char* serialize_string = cJSON_PrintUnformatted(serialize_array); - cJSON_Delete(serialize_array); - - if (!serialize_string) { - cJSON_Delete(event); + + signer = nostr_signer_local(private_key); + if (!signer) { return NULL; } - - // Hash the serialized event - unsigned char event_hash[32]; - if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) { - free(serialize_string); - cJSON_Delete(event); - return NULL; - } - - // Convert hash to hex for event ID - char event_id[65]; - nostr_bytes_to_hex(event_hash, 32, event_id); - - // Sign the hash using ECDSA - unsigned char signature[64]; - if (nostr_ec_sign(private_key, event_hash, signature) != 0) { - free(serialize_string); - cJSON_Delete(event); - return NULL; - } - - // Convert signature to hex - char sig_hex[129]; - nostr_bytes_to_hex(signature, 64, sig_hex); - - // Add ID and signature to the event - cJSON_AddStringToObject(event, "id", event_id); - cJSON_AddStringToObject(event, "sig", sig_hex); - - free(serialize_string); - - return event; + + out = nostr_create_and_sign_event_with_signer(kind, content, tags, signer, timestamp); + nostr_signer_free(signer); + return out; } /** diff --git a/nostr_core/nip001.h b/nostr_core/nip001.h index 3595176d..4eddf134 100644 --- a/nostr_core/nip001.h +++ b/nostr_core/nip001.h @@ -10,10 +10,12 @@ #include #include #include "../cjson/cJSON.h" +#include "nostr_signer.h" // Function declarations cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp); // Event validation functions int nostr_validate_event_structure(cJSON* event); diff --git a/nostr_core/nip006.c b/nostr_core/nip006.c index 26672c8d..691fffcf 100644 --- a/nostr_core/nip006.c +++ b/nostr_core/nip006.c @@ -10,24 +10,17 @@ #include #include #include "../nostr_core/nostr_common.h" +#include "nostr_platform.h" int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key) { if (!private_key || !public_key) { return NOSTR_ERROR_INVALID_INPUT; } - // Generate random entropy using /dev/urandom - FILE* urandom = fopen("/dev/urandom", "rb"); - if (!urandom) { + if (nostr_platform_random(private_key, 32) != 0) { return NOSTR_ERROR_IO_FAILED; } - if (fread(private_key, 1, 32, urandom) != 32) { - fclose(urandom); - return NOSTR_ERROR_IO_FAILED; - } - fclose(urandom); - // Validate private key if (nostr_ec_private_key_verify(private_key) != 0) { return NOSTR_ERROR_CRYPTO_FAILED; @@ -50,17 +43,10 @@ int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size, // Generate entropy for 12-word mnemonic unsigned char entropy[16]; - FILE* urandom = fopen("/dev/urandom", "rb"); - if (!urandom) { + if (nostr_platform_random(entropy, sizeof(entropy)) != 0) { return NOSTR_ERROR_IO_FAILED; } - if (fread(entropy, 1, sizeof(entropy), urandom) != sizeof(entropy)) { - fclose(urandom); - return NOSTR_ERROR_IO_FAILED; - } - fclose(urandom); - // Generate mnemonic from entropy if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) { return NOSTR_ERROR_CRYPTO_FAILED; diff --git a/nostr_core/nip017.c b/nostr_core/nip017.c index 0915af38..e3a6b310 100644 --- a/nostr_core/nip017.c +++ b/nostr_core/nip017.c @@ -216,21 +216,34 @@ cJSON* nostr_nip17_create_file_event(const char* file_url, cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls, int num_relays, const unsigned char* private_key) { - if (!relay_urls || num_relays <= 0 || !private_key) { + nostr_signer_t* signer; + cJSON* out; + + if (!private_key) { return NULL; } - // Get public key - unsigned char public_key[32]; - if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) { + signer = nostr_signer_local(private_key); + if (!signer) { return NULL; } - char pubkey_hex[65]; - nostr_bytes_to_hex(public_key, 32, pubkey_hex); + out = nostr_nip17_create_relay_list_event_with_signer(relay_urls, num_relays, signer); + nostr_signer_free(signer); + return out; +} - // Create tags with relay URLs - cJSON* tags = cJSON_CreateArray(); +cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, + int num_relays, + nostr_signer_t* signer) { + cJSON* tags; + cJSON* relay_event; + + if (!relay_urls || num_relays <= 0 || !signer) { + return NULL; + } + + tags = cJSON_CreateArray(); if (!tags) { return NULL; } @@ -246,11 +259,8 @@ cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls, cJSON_AddItemToArray(tags, relay_tag); } - // Create and sign the event - cJSON* relay_event = nostr_create_and_sign_event(10050, "", tags, private_key, time(NULL)); - - cJSON_Delete(tags); // Tags are duplicated in create_and_sign_event - + relay_event = nostr_create_and_sign_event_with_signer(10050, "", tags, signer, time(NULL)); + cJSON_Delete(tags); return relay_event; } @@ -264,49 +274,65 @@ int nostr_nip17_send_dm(cJSON* dm_event, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec) { - if (!dm_event || !recipient_pubkeys || num_recipients <= 0 || - !sender_private_key || !gift_wraps_out || max_gift_wraps <= 0) { + nostr_signer_t* signer; + int out; + + if (!sender_private_key) { return -1; } + signer = nostr_signer_local(sender_private_key); + if (!signer) { + return -1; + } + + out = nostr_nip17_send_dm_with_signer(dm_event, recipient_pubkeys, num_recipients, signer, + gift_wraps_out, max_gift_wraps, max_delay_sec); + nostr_signer_free(signer); + return out; +} + +int nostr_nip17_send_dm_with_signer(cJSON* dm_event, + const char** recipient_pubkeys, + int num_recipients, + nostr_signer_t* signer, + cJSON** gift_wraps_out, + int max_gift_wraps, + long max_delay_sec) { int created_wraps = 0; + char sender_pubkey_hex[65]; + + if (!dm_event || !recipient_pubkeys || num_recipients <= 0 || + !signer || !gift_wraps_out || max_gift_wraps <= 0) { + return -1; + } for (int i = 0; i < num_recipients && created_wraps < max_gift_wraps; i++) { - // Convert recipient pubkey hex to bytes unsigned char recipient_public_key[32]; if (nostr_hex_to_bytes(recipient_pubkeys[i], recipient_public_key, 32) != 0) { - continue; // Skip invalid pubkeys + continue; } - // Create seal for this recipient - cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec); + cJSON* seal = nostr_nip59_create_seal_with_signer(dm_event, signer, recipient_public_key, max_delay_sec); if (!seal) { - continue; // Skip if sealing fails + continue; } - // Create gift wrap for this recipient cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec); - cJSON_Delete(seal); // Seal is now wrapped + cJSON_Delete(seal); if (!gift_wrap) { - continue; // Skip if wrapping fails + continue; } gift_wraps_out[created_wraps++] = gift_wrap; } - // Also create a gift wrap for the sender (so they can see their own messages) - if (created_wraps < max_gift_wraps) { - // Get sender's public key + if (created_wraps < max_gift_wraps && nostr_signer_get_public_key(signer, sender_pubkey_hex) == NOSTR_SUCCESS) { unsigned char sender_public_key[32]; - if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) == 0) { - char sender_pubkey_hex[65]; - nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex); - - // Create seal for sender - cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec); + if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) == 0) { + cJSON* sender_seal = nostr_nip59_create_seal_with_signer(dm_event, signer, sender_public_key, max_delay_sec); if (sender_seal) { - // Create gift wrap for sender cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec); cJSON_Delete(sender_seal); @@ -325,48 +351,74 @@ int nostr_nip17_send_dm(cJSON* dm_event, */ cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap, const unsigned char* recipient_private_key) { - if (!gift_wrap || !recipient_private_key) { + nostr_signer_t* signer; + cJSON* out; + + if (!recipient_private_key) { return NULL; } - // Unwrap the gift wrap to get the seal - cJSON* seal = nostr_nip59_unwrap_gift(gift_wrap, recipient_private_key); + signer = nostr_signer_local(recipient_private_key); + if (!signer) { + return NULL; + } + + out = nostr_nip17_receive_dm_with_signer(gift_wrap, signer); + nostr_signer_free(signer); + return out; +} + +cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap, + nostr_signer_t* signer) { + cJSON* seal; + cJSON* seal_pubkey_item; + const char* sender_pubkey_hex; + unsigned char sender_public_key[32]; + char sender_pubkey_hex_copy[65]; + cJSON* rumor; + cJSON* rumor_pubkey_item; + const char* rumor_pubkey_hex; + + if (!gift_wrap || !signer) { + return NULL; + } + + seal = nostr_nip59_unwrap_gift_with_signer(gift_wrap, signer); if (!seal) { return NULL; } - // Get sender's public key from the seal - cJSON* seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey"); + seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey"); if (!seal_pubkey_item || !cJSON_IsString(seal_pubkey_item)) { cJSON_Delete(seal); return NULL; } - const char* sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item); - - // Convert sender pubkey hex to bytes - unsigned char sender_public_key[32]; - if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) { + sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item); + if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64) { + cJSON_Delete(seal); + return NULL; + } + memcpy(sender_pubkey_hex_copy, sender_pubkey_hex, 65); + if (nostr_hex_to_bytes(sender_pubkey_hex_copy, sender_public_key, 32) != 0) { cJSON_Delete(seal); return NULL; } - // Unseal the rumor - cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key); - cJSON_Delete(seal); // Seal is no longer needed + rumor = nostr_nip59_unseal_rumor_with_signer(seal, sender_public_key, signer); + cJSON_Delete(seal); if (!rumor) { return NULL; } - // NIP-17 safety check: seal pubkey must match rumor pubkey to prevent impersonation - cJSON* rumor_pubkey_item = cJSON_GetObjectItem(rumor, "pubkey"); + rumor_pubkey_item = cJSON_GetObjectItem(rumor, "pubkey"); if (!rumor_pubkey_item || !cJSON_IsString(rumor_pubkey_item)) { cJSON_Delete(rumor); return NULL; } - const char* rumor_pubkey_hex = cJSON_GetStringValue(rumor_pubkey_item); - if (!rumor_pubkey_hex || strcmp(sender_pubkey_hex, rumor_pubkey_hex) != 0) { + rumor_pubkey_hex = cJSON_GetStringValue(rumor_pubkey_item); + if (!rumor_pubkey_hex || strcmp(sender_pubkey_hex_copy, rumor_pubkey_hex) != 0) { cJSON_Delete(rumor); return NULL; } diff --git a/nostr_core/nip017.h b/nostr_core/nip017.h index 6462cc53..a64144e3 100644 --- a/nostr_core/nip017.h +++ b/nostr_core/nip017.h @@ -7,6 +7,7 @@ #define NOSTR_NIP017_H #include +#include "nostr_signer.h" #include "../cjson/cJSON.h" #ifdef __cplusplus @@ -84,6 +85,9 @@ cJSON* nostr_nip17_create_file_event(const char* file_url, cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls, int num_relays, const unsigned char* private_key); +cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, + int num_relays, + nostr_signer_t* signer); /** * NIP-17: Send a direct message to recipients @@ -107,6 +111,13 @@ int nostr_nip17_send_dm(cJSON* dm_event, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec); +int nostr_nip17_send_dm_with_signer(cJSON* dm_event, + const char** recipient_pubkeys, + int num_recipients, + nostr_signer_t* signer, + cJSON** gift_wraps_out, + int max_gift_wraps, + long max_delay_sec); /** * NIP-17: Receive and decrypt a direct message @@ -119,6 +130,8 @@ int nostr_nip17_send_dm(cJSON* dm_event, */ cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap, const unsigned char* recipient_private_key); +cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap, + nostr_signer_t* signer); /** * NIP-17: Extract DM relay URLs from a user's kind 10050 event diff --git a/nostr_core/nip019.c b/nostr_core/nip019.c index a538ed7b..12ca5da5 100644 --- a/nostr_core/nip019.c +++ b/nostr_core/nip019.c @@ -180,7 +180,7 @@ nostr_input_type_t nostr_detect_input_type(const char* input) { if (len == 64) { int is_hex = 1; for (size_t i = 0; i < len; i++) { - if (!isxdigit(input[i])) { + if (!isxdigit((unsigned char)input[i])) { is_hex = 0; break; } diff --git a/nostr_core/nip042.c b/nostr_core/nip042.c index 60398c44..2880f34f 100644 --- a/nostr_core/nip042.c +++ b/nostr_core/nip042.c @@ -23,17 +23,17 @@ int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len); /** * Create NIP-42 authentication event (kind 22242) */ -cJSON* nostr_nip42_create_auth_event(const char* challenge, - const char* relay_url, - const unsigned char* private_key, - time_t timestamp) { - if (!challenge || !relay_url || !private_key) { +cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge, + const char* relay_url, + nostr_signer_t* signer, + time_t timestamp) { + if (!challenge || !relay_url || !signer) { return NULL; } // Validate challenge format size_t challenge_len = strlen(challenge); - if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH || + if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH || challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) { return NULL; } @@ -64,13 +64,11 @@ cJSON* nostr_nip42_create_auth_event(const char* challenge, cJSON_AddItemToArray(challenge_tag, cJSON_CreateString(challenge)); cJSON_AddItemToArray(tags, challenge_tag); - // Create authentication event using existing function - // Note: Empty content as per NIP-42 specification - cJSON* auth_event = nostr_create_and_sign_event( + cJSON* auth_event = nostr_create_and_sign_event_with_signer( NOSTR_NIP42_AUTH_EVENT_KIND, - "", // Empty content + "", tags, - private_key, + signer, timestamp ); @@ -78,6 +76,27 @@ cJSON* nostr_nip42_create_auth_event(const char* challenge, return auth_event; } +cJSON* nostr_nip42_create_auth_event(const char* challenge, + const char* relay_url, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) { + return NULL; + } + + signer = nostr_signer_local(private_key); + if (!signer) { + return NULL; + } + + evt = nostr_nip42_create_auth_event_with_signer(challenge, relay_url, signer, timestamp); + nostr_signer_free(signer); + return evt; +} + /** * Create AUTH message JSON for relay communication */ diff --git a/nostr_core/nip042.h b/nostr_core/nip042.h index b565920d..bebd50ef 100644 --- a/nostr_core/nip042.h +++ b/nostr_core/nip042.h @@ -12,6 +12,7 @@ #include #include "../cjson/cJSON.h" #include "nostr_common.h" +#include "nostr_signer.h" #ifdef __cplusplus extern "C" { @@ -64,11 +65,16 @@ typedef struct { * @param timestamp Event timestamp (0 for current time) * @return cJSON event object or NULL on error */ -cJSON* nostr_nip42_create_auth_event(const char* challenge, +cJSON* nostr_nip42_create_auth_event(const char* challenge, const char* relay_url, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge, + const char* relay_url, + nostr_signer_t* signer, + time_t timestamp); + /** * Create AUTH message JSON for relay communication * @param auth_event Authentication event (kind 22242) diff --git a/nostr_core/nip046.c b/nostr_core/nip046.c index b42aed13..aa24ae63 100644 --- a/nostr_core/nip046.c +++ b/nostr_core/nip046.c @@ -360,12 +360,12 @@ int nostr_nip46_parse_response(const char* json_payload, nostr_nip46_response_t* return NOSTR_SUCCESS; } -static cJSON* create_nip46_event(int kind, - const char* encrypted_content, - const unsigned char* sender_private_key, - const unsigned char* recipient_public_key, - time_t timestamp) { - if (!encrypted_content || !sender_private_key || !recipient_public_key) return NULL; +static cJSON* create_nip46_event_with_signer(int kind, + const char* encrypted_content, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp) { + if (!encrypted_content || !signer || !recipient_public_key) return NULL; char recipient_hex[65]; nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex); @@ -383,7 +383,7 @@ static cJSON* create_nip46_event(int kind, cJSON_AddItemToArray(ptag, cJSON_CreateString(recipient_hex)); cJSON_AddItemToArray(tags, ptag); - cJSON* evt = nostr_create_and_sign_event(kind, encrypted_content, tags, sender_private_key, timestamp); + cJSON* evt = nostr_create_and_sign_event_with_signer(kind, encrypted_content, tags, signer, timestamp); cJSON_Delete(tags); return evt; } @@ -392,48 +392,129 @@ cJSON* nostr_nip46_create_request_event(const nostr_nip46_request_t* request, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, time_t timestamp) { - if (!request || !sender_private_key || !recipient_public_key) return NULL; + nostr_signer_t* signer; + cJSON* out; + + if (!sender_private_key) return NULL; + signer = nostr_signer_local(sender_private_key); + if (!signer) return NULL; + + out = nostr_nip46_create_request_event_with_signer(request, signer, recipient_public_key, timestamp); + nostr_signer_free(signer); + return out; +} + +cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp) { + if (!request || !signer || !recipient_public_key) return NULL; char* json_payload = NULL; if (nostr_nip46_request_to_json(request, &json_payload) != NOSTR_SUCCESS) return NULL; - char encrypted[NOSTR_NIP46_MAX_PAYLOAD_LEN]; - int rc = nostr_nip44_encrypt(sender_private_key, recipient_public_key, json_payload, - encrypted, sizeof(encrypted)); - free(json_payload); - if (rc != NOSTR_SUCCESS) return NULL; + char recipient_hex[65]; + nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex); - return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted, - sender_private_key, recipient_public_key, timestamp); + char* encrypted = NULL; + int rc = nostr_signer_nip44_encrypt(signer, recipient_hex, json_payload, &encrypted); + free(json_payload); + if (rc != NOSTR_SUCCESS || !encrypted) { + free(encrypted); + return NULL; + } + + cJSON* out = create_nip46_event_with_signer(NOSTR_NIP46_EVENT_KIND, encrypted, + signer, recipient_public_key, timestamp); + free(encrypted); + return out; } cJSON* nostr_nip46_create_response_event(const nostr_nip46_response_t* response, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, time_t timestamp) { - if (!response || !sender_private_key || !recipient_public_key) return NULL; + nostr_signer_t* signer; + cJSON* out; + + if (!sender_private_key) return NULL; + signer = nostr_signer_local(sender_private_key); + if (!signer) return NULL; + + out = nostr_nip46_create_response_event_with_signer(response, signer, recipient_public_key, timestamp); + nostr_signer_free(signer); + return out; +} + +cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp) { + if (!response || !signer || !recipient_public_key) return NULL; char* json_payload = NULL; if (nostr_nip46_response_to_json(response, &json_payload) != NOSTR_SUCCESS) return NULL; - char encrypted[NOSTR_NIP46_MAX_PAYLOAD_LEN]; - int rc = nostr_nip44_encrypt(sender_private_key, recipient_public_key, json_payload, - encrypted, sizeof(encrypted)); - free(json_payload); - if (rc != NOSTR_SUCCESS) return NULL; + char recipient_hex[65]; + nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex); - return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted, - sender_private_key, recipient_public_key, timestamp); + char* encrypted = NULL; + int rc = nostr_signer_nip44_encrypt(signer, recipient_hex, json_payload, &encrypted); + free(json_payload); + if (rc != NOSTR_SUCCESS || !encrypted) { + free(encrypted); + return NULL; + } + + cJSON* out = create_nip46_event_with_signer(NOSTR_NIP46_EVENT_KIND, encrypted, + signer, recipient_public_key, timestamp); + free(encrypted); + return out; } int nostr_nip46_decrypt_event(cJSON* event, const unsigned char* recipient_private_key, char* output, size_t output_size) { - if (!event || !recipient_private_key || !output || output_size == 0) { + nostr_signer_t* signer; + char* decrypted = NULL; + int rc; + + if (!recipient_private_key || !output || output_size == 0) { return NOSTR_ERROR_INVALID_INPUT; } + signer = nostr_signer_local(recipient_private_key); + if (!signer) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + rc = nostr_nip46_decrypt_event_with_signer(event, signer, &decrypted); + nostr_signer_free(signer); + if (rc != NOSTR_SUCCESS || !decrypted) { + free(decrypted); + return NOSTR_ERROR_NIP46_DECRYPTION_FAILED; + } + + if (strlen(decrypted) + 1 > output_size) { + free(decrypted); + return NOSTR_ERROR_INVALID_INPUT; + } + + strcpy(output, decrypted); + free(decrypted); + return NOSTR_SUCCESS; +} + +int nostr_nip46_decrypt_event_with_signer(cJSON* event, + nostr_signer_t* signer, + char** output_out) { + if (!event || !signer || !output_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *output_out = NULL; + cJSON* content = cJSON_GetObjectItem(event, "content"); cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey"); cJSON* kind = cJSON_GetObjectItem(event, "kind"); @@ -446,14 +527,10 @@ int nostr_nip46_decrypt_event(cJSON* event, return NOSTR_ERROR_NIP46_INVALID_REQUEST; } - const char* sender_hex = cJSON_GetStringValue(pubkey); - unsigned char sender_pubkey[32]; - if (nostr_hex_to_bytes(sender_hex, sender_pubkey, 32) != 0) { - return NOSTR_ERROR_NIP46_DECRYPTION_FAILED; - } - - int rc = nostr_nip44_decrypt(recipient_private_key, sender_pubkey, - cJSON_GetStringValue(content), output, output_size); + int rc = nostr_signer_nip44_decrypt(signer, + cJSON_GetStringValue(pubkey), + cJSON_GetStringValue(content), + output_out); if (rc != NOSTR_SUCCESS) return NOSTR_ERROR_NIP46_DECRYPTION_FAILED; return NOSTR_SUCCESS; } @@ -715,23 +792,48 @@ int nostr_nip46_create_nostrconnect_url(const nostr_nip46_nostrconnect_url_t* in int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session, const unsigned char* client_private_key, const char* bunker_url) { - if (!session || !client_private_key || !bunker_url) return NOSTR_ERROR_INVALID_INPUT; + nostr_signer_t* signer; + int rc; + + if (!client_private_key) return NOSTR_ERROR_INVALID_INPUT; + signer = nostr_signer_local(client_private_key); + if (!signer) return NOSTR_ERROR_CRYPTO_FAILED; + + rc = nostr_nip46_client_session_init_with_signer(session, signer, bunker_url); + if (rc != NOSTR_SUCCESS) { + nostr_signer_free(signer); + return rc; + } + + session->owns_client_signer = 1; + return NOSTR_SUCCESS; +} + +int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session, + nostr_signer_t* signer, + const char* bunker_url) { + if (!session || !signer || !bunker_url) return NOSTR_ERROR_INVALID_INPUT; memset(session, 0, sizeof(*session)); - memcpy(session->client_private_key, client_private_key, 32); + session->client_signer = signer; + session->owns_client_signer = 0; - unsigned char client_pub[32]; - if (nostr_ec_public_key_from_private_key(client_private_key, client_pub) != 0) { + int rc = nostr_signer_get_public_key(signer, session->client_pubkey_hex); + if (rc != NOSTR_SUCCESS) { + memset(session, 0, sizeof(*session)); return NOSTR_ERROR_CRYPTO_FAILED; } - nostr_bytes_to_hex(client_pub, 32, session->client_pubkey_hex); nostr_nip46_bunker_url_t parsed; - int rc = nostr_nip46_parse_bunker_url(bunker_url, &parsed); - if (rc != NOSTR_SUCCESS) return rc; + rc = nostr_nip46_parse_bunker_url(bunker_url, &parsed); + if (rc != NOSTR_SUCCESS) { + memset(session, 0, sizeof(*session)); + return rc; + } safe_copy(session->remote_signer_pubkey_hex, sizeof(session->remote_signer_pubkey_hex), parsed.remote_signer_pubkey); if (nostr_hex_to_bytes(parsed.remote_signer_pubkey, session->remote_signer_pubkey, 32) != 0) { + memset(session, 0, sizeof(*session)); return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL; } @@ -746,6 +848,9 @@ int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session, void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session) { if (!session) return; + if (session->owns_client_signer && session->client_signer) { + nostr_signer_free(session->client_signer); + } memset(session, 0, sizeof(*session)); } @@ -754,7 +859,7 @@ static int client_make_request_event(nostr_nip46_client_session_t* session, const char** params, int param_count, cJSON** request_event_out) { - if (!session || !request_event_out) return NOSTR_ERROR_INVALID_INPUT; + if (!session || !session->client_signer || !request_event_out) return NOSTR_ERROR_INVALID_INPUT; char req_id[NOSTR_NIP46_MAX_REQUEST_ID_LEN]; int rc = nostr_nip46_generate_request_id(req_id, sizeof(req_id)); @@ -764,8 +869,8 @@ static int client_make_request_event(nostr_nip46_client_session_t* session, rc = nostr_nip46_create_request(req_id, method, params, param_count, &req); if (rc != NOSTR_SUCCESS) return rc; - cJSON* evt = nostr_nip46_create_request_event(&req, session->client_private_key, - session->remote_signer_pubkey, 0); + cJSON* evt = nostr_nip46_create_request_event_with_signer(&req, session->client_signer, + session->remote_signer_pubkey, 0); nostr_nip46_free_request(&req); if (!evt) return NOSTR_ERROR_NIP46_ENCRYPTION_FAILED; diff --git a/nostr_core/nip046.h b/nostr_core/nip046.h index e51b634c..a05b0c70 100644 --- a/nostr_core/nip046.h +++ b/nostr_core/nip046.h @@ -10,6 +10,7 @@ #include #include "nostr_common.h" #include "nip001.h" +#include "nostr_signer.h" #include "../cjson/cJSON.h" #ifdef __cplusplus @@ -83,6 +84,8 @@ typedef struct { char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN]; int relay_count; int connected; + nostr_signer_t* client_signer; + int owns_client_signer; } nostr_nip46_client_session_t; typedef struct { @@ -132,19 +135,33 @@ cJSON* nostr_nip46_create_request_event(const nostr_nip46_request_t* request, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, time_t timestamp); +cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp); cJSON* nostr_nip46_create_response_event(const nostr_nip46_response_t* response, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, time_t timestamp); +cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response, + nostr_signer_t* signer, + const unsigned char* recipient_public_key, + time_t timestamp); int nostr_nip46_decrypt_event(cJSON* event, const unsigned char* recipient_private_key, char* output, size_t output_size); +int nostr_nip46_decrypt_event_with_signer(cJSON* event, + nostr_signer_t* signer, + char** output_out); /* Client session */ int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session, const unsigned char* client_private_key, const char* bunker_url); +int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session, + nostr_signer_t* signer, + const char* bunker_url); void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session); int nostr_nip46_client_connect(nostr_nip46_client_session_t* session, diff --git a/nostr_core/nip059.c b/nostr_core/nip059.c index 9cd43787..d05fdd66 100644 --- a/nostr_core/nip059.c +++ b/nostr_core/nip059.c @@ -182,90 +182,79 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags, */ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, long max_delay_sec) { - if (!rumor || !sender_private_key || !recipient_public_key) { + nostr_signer_t* signer; + cJSON* out; + + if (!sender_private_key) { return NULL; } - // Serialize the rumor to JSON - char* rumor_json = cJSON_PrintUnformatted(rumor); + signer = nostr_signer_local(sender_private_key); + if (!signer) { + return NULL; + } + + out = nostr_nip59_create_seal_with_signer(rumor, signer, recipient_public_key, max_delay_sec); + nostr_signer_free(signer); + return out; +} + +cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer, + const unsigned char* recipient_public_key, long max_delay_sec) { + cJSON* seal; + cJSON* signed_seal = NULL; + char* rumor_json; + char recipient_pubkey_hex[65]; + char sender_pubkey_hex[65]; + char* encrypted_content = NULL; + int rc; + time_t seal_time; + + if (!rumor || !signer || !recipient_public_key) { + return NULL; + } + + rumor_json = cJSON_PrintUnformatted(rumor); if (!rumor_json) { return NULL; } - // Encrypt the rumor using NIP-44 - size_t encrypted_size = calc_nip44_encrypted_b64_size(strlen(rumor_json)); - if (encrypted_size == 0) { - free(rumor_json); - return NULL; - } - - char* encrypted_content = malloc(encrypted_size); - if (!encrypted_content) { - free(rumor_json); - return NULL; - } - - int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key, - rumor_json, encrypted_content, encrypted_size); + nostr_bytes_to_hex(recipient_public_key, 32, recipient_pubkey_hex); + rc = nostr_signer_nip44_encrypt(signer, recipient_pubkey_hex, rumor_json, &encrypted_content); free(rumor_json); - - if (encrypt_result != NOSTR_SUCCESS) { + if (rc != NOSTR_SUCCESS || !encrypted_content) { free(encrypted_content); return NULL; } - // Get sender's public key - unsigned char sender_public_key[32]; - if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) { + rc = nostr_signer_get_public_key(signer, sender_pubkey_hex); + if (rc != NOSTR_SUCCESS) { free(encrypted_content); return NULL; } - char sender_pubkey_hex[65]; - nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex); - - // Create seal event (kind 13) - cJSON* seal = cJSON_CreateObject(); + seal = cJSON_CreateObject(); if (!seal) { free(encrypted_content); return NULL; } - time_t seal_time = random_past_timestamp(max_delay_sec); - + seal_time = random_past_timestamp(max_delay_sec); cJSON_AddStringToObject(seal, "pubkey", sender_pubkey_hex); cJSON_AddNumberToObject(seal, "created_at", (double)seal_time); cJSON_AddNumberToObject(seal, "kind", 13); - cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); // Empty tags array + cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); cJSON_AddStringToObject(seal, "content", encrypted_content); free(encrypted_content); - // Calculate event ID - char event_id[65]; - if (create_event_id(seal, event_id) != 0) { - cJSON_Delete(seal); - return NULL; - } - cJSON_AddStringToObject(seal, "id", event_id); - - // Sign the seal - unsigned char event_hash[32]; - if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) { - cJSON_Delete(seal); + rc = nostr_signer_sign_event(signer, seal, &signed_seal); + cJSON_Delete(seal); + if (rc != NOSTR_SUCCESS) { + cJSON_Delete(signed_seal); return NULL; } - unsigned char signature[64]; - if (nostr_ec_sign(sender_private_key, event_hash, signature) != 0) { - cJSON_Delete(seal); - return NULL; - } - - char sig_hex[129]; - nostr_bytes_to_hex(signature, 64, sig_hex); - cJSON_AddStringToObject(seal, "sig", sig_hex); - - return seal; + return signed_seal; } /** @@ -396,54 +385,53 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke * NIP-59: Unwrap a gift wrap to get the seal */ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key) { - if (!gift_wrap || !recipient_private_key) { + nostr_signer_t* signer; + cJSON* out; + + if (!recipient_private_key) { return NULL; } - // Get the encrypted content - cJSON* content_item = cJSON_GetObjectItem(gift_wrap, "content"); - if (!content_item || !cJSON_IsString(content_item)) { + signer = nostr_signer_local(recipient_private_key); + if (!signer) { return NULL; } - const char* encrypted_content = cJSON_GetStringValue(content_item); + out = nostr_nip59_unwrap_gift_with_signer(gift_wrap, signer); + nostr_signer_free(signer); + return out; +} - // Get the sender's public key (gift wrap pubkey) - cJSON* pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey"); - if (!pubkey_item || !cJSON_IsString(pubkey_item)) { +cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer) { + cJSON* content_item; + cJSON* pubkey_item; + const char* encrypted_content; + const char* sender_pubkey_hex; + char* decrypted_json = NULL; + int rc; + cJSON* seal; + + if (!gift_wrap || !signer) { return NULL; } - const char* sender_pubkey_hex = cJSON_GetStringValue(pubkey_item); - - // Convert sender pubkey hex to bytes - unsigned char sender_public_key[32]; - if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) { + content_item = cJSON_GetObjectItem(gift_wrap, "content"); + pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey"); + if (!content_item || !cJSON_IsString(content_item) || !pubkey_item || !cJSON_IsString(pubkey_item)) { return NULL; } - // Decrypt the content using NIP-44 - size_t decrypted_size = strlen(encrypted_content) + 1; - char* decrypted_json = malloc(decrypted_size); - if (!decrypted_json) { - return NULL; - } + encrypted_content = cJSON_GetStringValue(content_item); + sender_pubkey_hex = cJSON_GetStringValue(pubkey_item); - int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key, - encrypted_content, decrypted_json, decrypted_size); - - if (decrypt_result != NOSTR_SUCCESS) { + rc = nostr_signer_nip44_decrypt(signer, sender_pubkey_hex, encrypted_content, &decrypted_json); + if (rc != NOSTR_SUCCESS || !decrypted_json) { free(decrypted_json); return NULL; } - // Parse the decrypted JSON as the seal event - cJSON* seal = cJSON_Parse(decrypted_json); + seal = cJSON_Parse(decrypted_json); free(decrypted_json); - if (!seal) { - return NULL; - } - return seal; } @@ -452,39 +440,51 @@ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_ */ cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key, const unsigned char* recipient_private_key) { - if (!seal || !sender_public_key || !recipient_private_key) { + nostr_signer_t* signer; + cJSON* out; + + if (!recipient_private_key) { return NULL; } - // Get the encrypted content - cJSON* content_item = cJSON_GetObjectItem(seal, "content"); + signer = nostr_signer_local(recipient_private_key); + if (!signer) { + return NULL; + } + + out = nostr_nip59_unseal_rumor_with_signer(seal, sender_public_key, signer); + nostr_signer_free(signer); + return out; +} + +cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key, + nostr_signer_t* signer) { + cJSON* content_item; + const char* encrypted_content; + char sender_pubkey_hex[65]; + char* decrypted_json = NULL; + int rc; + cJSON* rumor; + + if (!seal || !sender_public_key || !signer) { + return NULL; + } + + content_item = cJSON_GetObjectItem(seal, "content"); if (!content_item || !cJSON_IsString(content_item)) { return NULL; } - const char* encrypted_content = cJSON_GetStringValue(content_item); + encrypted_content = cJSON_GetStringValue(content_item); + nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex); - // Decrypt the content using NIP-44 - size_t decrypted_size = strlen(encrypted_content) + 1; - char* decrypted_json = malloc(decrypted_size); - if (!decrypted_json) { - return NULL; - } - - int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key, - encrypted_content, decrypted_json, decrypted_size); - - if (decrypt_result != NOSTR_SUCCESS) { + rc = nostr_signer_nip44_decrypt(signer, sender_pubkey_hex, encrypted_content, &decrypted_json); + if (rc != NOSTR_SUCCESS || !decrypted_json) { free(decrypted_json); return NULL; } - // Parse the decrypted JSON as the rumor event - cJSON* rumor = cJSON_Parse(decrypted_json); + rumor = cJSON_Parse(decrypted_json); free(decrypted_json); - if (!rumor) { - return NULL; - } - return rumor; } \ No newline at end of file diff --git a/nostr_core/nip059.h b/nostr_core/nip059.h index 4ae31997..759948c6 100644 --- a/nostr_core/nip059.h +++ b/nostr_core/nip059.h @@ -8,6 +8,7 @@ #include #include +#include "nostr_signer.h" #include "../cjson/cJSON.h" #ifdef __cplusplus @@ -38,6 +39,8 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags, */ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, long max_delay_sec); +cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer, + const unsigned char* recipient_public_key, long max_delay_sec); /** * NIP-59: Create a gift wrap (kind 1059) wrapping a seal @@ -57,6 +60,7 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke * @return cJSON object representing the seal event, or NULL on error */ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key); +cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer); /** * NIP-59: Unseal a seal to get the rumor @@ -68,6 +72,8 @@ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_ */ cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key, const unsigned char* recipient_private_key); +cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key, + nostr_signer_t* signer); #ifdef __cplusplus } diff --git a/nostr_core/nip060.c b/nostr_core/nip060.c index 8ca8b413..ba2f31b1 100644 --- a/nostr_core/nip060.c +++ b/nostr_core/nip060.c @@ -47,20 +47,56 @@ static nostr_nip60_direction_t nip60_string_to_direction(const char* s) { return NOSTR_NIP60_DIRECTION_IN; } +static int nip60_encrypt_self_with_signer(nostr_signer_t* signer, + const char* plaintext, + char* output, + size_t output_size) { + if (!signer || !plaintext || !output || output_size == 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + char pubkey_hex[65]; + int rc = nostr_signer_get_public_key(signer, pubkey_hex); + if (rc != NOSTR_SUCCESS) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + char* encrypted = NULL; + rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &encrypted); + if (rc != NOSTR_SUCCESS || !encrypted) { + free(encrypted); + return NOSTR_ERROR_CRYPTO_FAILED; + } + + if (strlen(encrypted) + 1 > output_size) { + free(encrypted); + return NOSTR_ERROR_INVALID_INPUT; + } + + strcpy(output, encrypted); + free(encrypted); + return NOSTR_SUCCESS; +} + static int nip60_encrypt_self(const unsigned char* private_key, const char* plaintext, char* output, size_t output_size) { - if (!private_key || !plaintext || !output || output_size == 0) { + nostr_signer_t* signer; + int rc; + + if (!private_key) { return NOSTR_ERROR_INVALID_INPUT; } - unsigned char pubkey[32]; - if (nostr_ec_public_key_from_private_key(private_key, pubkey) != 0) { + signer = nostr_signer_local(private_key); + if (!signer) { return NOSTR_ERROR_CRYPTO_FAILED; } - return nostr_nip44_encrypt(private_key, pubkey, plaintext, output, output_size); + rc = nip60_encrypt_self_with_signer(signer, plaintext, output, output_size); + nostr_signer_free(signer); + return rc; } static int nip60_decrypt_event_content(cJSON* event, @@ -205,10 +241,10 @@ int nostr_nip60_proofs_from_json(cJSON* json_array, return NOSTR_SUCCESS; } -cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data, - const unsigned char* private_key, - time_t timestamp) { - if (!wallet_data || !private_key || wallet_data->mint_count <= 0) { +cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data, + nostr_signer_t* signer, + time_t timestamp) { + if (!wallet_data || !signer || wallet_data->mint_count <= 0) { return NULL; } @@ -241,11 +277,31 @@ cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_d if (!plain) return NULL; char encrypted[65536]; - int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted)); + int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted)); free(plain); if (rc != NOSTR_SUCCESS) return NULL; - return nostr_create_and_sign_event(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, private_key, timestamp); + return nostr_create_and_sign_event_with_signer(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, signer, timestamp); +} + +cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) { + return NULL; + } + + signer = nostr_signer_local(private_key); + if (!signer) { + return NULL; + } + + evt = nostr_nip60_create_wallet_event_with_signer(wallet_data, signer, timestamp); + nostr_signer_free(signer); + return evt; } int nostr_nip60_parse_wallet_event(cJSON* event, @@ -338,10 +394,10 @@ void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data) { memset(data, 0, sizeof(*data)); } -cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data, - const unsigned char* private_key, - time_t timestamp) { - if (!token_data || !private_key || !token_data->mint_url || +cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data, + nostr_signer_t* signer, + time_t timestamp) { + if (!token_data || !signer || !token_data->mint_url || !token_data->proofs || token_data->proof_count <= 0) { return NULL; } @@ -377,11 +433,31 @@ cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data if (!plain) return NULL; char encrypted[131072]; - int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted)); + int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted)); free(plain); if (rc != NOSTR_SUCCESS) return NULL; - return nostr_create_and_sign_event(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, private_key, timestamp); + return nostr_create_and_sign_event_with_signer(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, signer, timestamp); +} + +cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) { + return NULL; + } + + signer = nostr_signer_local(private_key); + if (!signer) { + return NULL; + } + + evt = nostr_nip60_create_token_event_with_signer(token_data, signer, timestamp); + nostr_signer_free(signer); + return evt; } int nostr_nip60_parse_token_event(cJSON* event, @@ -476,10 +552,10 @@ void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data) { memset(data, 0, sizeof(*data)); } -cJSON* nostr_nip60_create_token_deletion(const char* token_event_id, - const unsigned char* private_key, - time_t timestamp) { - if (!token_event_id || !private_key) return NULL; +cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id, + nostr_signer_t* signer, + time_t timestamp) { + if (!token_event_id || !signer) return NULL; cJSON* tags = cJSON_CreateArray(); if (!tags) return NULL; @@ -494,29 +570,64 @@ cJSON* nostr_nip60_create_token_deletion(const char* token_event_id, cJSON_AddItemToArray(k_tag, cJSON_CreateString("7375")); cJSON_AddItemToArray(tags, k_tag); - cJSON* evt = nostr_create_and_sign_event(5, "NIP-60 token spent", tags, private_key, timestamp); + cJSON* evt = nostr_create_and_sign_event_with_signer(5, "NIP-60 token spent", tags, signer, timestamp); cJSON_Delete(tags); return evt; } +cJSON* nostr_nip60_create_token_deletion(const char* token_event_id, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) return NULL; + + signer = nostr_signer_local(private_key); + if (!signer) return NULL; + + evt = nostr_nip60_create_token_deletion_with_signer(token_event_id, signer, timestamp); + nostr_signer_free(signer); + return evt; +} + +cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs, + const char** deleted_event_ids, + int deleted_count, + nostr_signer_t* signer, + time_t timestamp) { + if (!remaining_proofs || !signer) return NULL; + + nostr_nip60_token_data_t token = *remaining_proofs; + token.deleted_token_ids = (char**)deleted_event_ids; + token.deleted_count = deleted_count; + + return nostr_nip60_create_token_event_with_signer(&token, signer, timestamp); +} + cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs, const char** deleted_event_ids, int deleted_count, const unsigned char* private_key, time_t timestamp) { - if (!remaining_proofs || !private_key) return NULL; + nostr_signer_t* signer; + cJSON* evt; - nostr_nip60_token_data_t token = *remaining_proofs; - token.deleted_token_ids = (char**)deleted_event_ids; - token.deleted_count = deleted_count; + if (!private_key) return NULL; - return nostr_nip60_create_token_event(&token, private_key, timestamp); + signer = nostr_signer_local(private_key); + if (!signer) return NULL; + + evt = nostr_nip60_create_rollover_token_with_signer(remaining_proofs, deleted_event_ids, deleted_count, + signer, timestamp); + nostr_signer_free(signer); + return evt; } -cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data, - const unsigned char* private_key, - time_t timestamp) { - if (!history_data || !private_key) return NULL; +cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data, + nostr_signer_t* signer, + time_t timestamp) { + if (!history_data || !signer) return NULL; cJSON* payload = cJSON_CreateArray(); if (!payload) return NULL; @@ -547,7 +658,7 @@ cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* histor if (!plain) return NULL; char encrypted[65536]; - int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted)); + int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted)); free(plain); if (rc != NOSTR_SUCCESS) return NULL; @@ -565,11 +676,27 @@ cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* histor cJSON_AddItemToArray(tags, e); } - cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, private_key, timestamp); + cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, signer, timestamp); cJSON_Delete(tags); return evt; } +cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) return NULL; + + signer = nostr_signer_local(private_key); + if (!signer) return NULL; + + evt = nostr_nip60_create_history_event_with_signer(history_data, signer, timestamp); + nostr_signer_free(signer); + return evt; +} + int nostr_nip60_parse_history_event(cJSON* event, const unsigned char* private_key, nostr_nip60_history_data_t* history_data_out) { @@ -665,17 +792,17 @@ void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data) { memset(data, 0, sizeof(*data)); } -cJSON* nostr_nip60_create_quote_event(const char* quote_id, - const char* mint_url, - time_t expiration, - const unsigned char* private_key, - time_t timestamp) { - if (!quote_id || !mint_url || !private_key || expiration <= 0) { +cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id, + const char* mint_url, + time_t expiration, + nostr_signer_t* signer, + time_t timestamp) { + if (!quote_id || !mint_url || !signer || expiration <= 0) { return NULL; } char encrypted[4096]; - int rc = nip60_encrypt_self(private_key, quote_id, encrypted, sizeof(encrypted)); + int rc = nip60_encrypt_self_with_signer(signer, quote_id, encrypted, sizeof(encrypted)); if (rc != NOSTR_SUCCESS) return NULL; cJSON* tags = cJSON_CreateArray(); @@ -694,11 +821,29 @@ cJSON* nostr_nip60_create_quote_event(const char* quote_id, cJSON_AddItemToArray(mint_tag, cJSON_CreateString(mint_url)); cJSON_AddItemToArray(tags, mint_tag); - cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, private_key, timestamp); + cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, signer, timestamp); cJSON_Delete(tags); return evt; } +cJSON* nostr_nip60_create_quote_event(const char* quote_id, + const char* mint_url, + time_t expiration, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) return NULL; + + signer = nostr_signer_local(private_key); + if (!signer) return NULL; + + evt = nostr_nip60_create_quote_event_with_signer(quote_id, mint_url, expiration, signer, timestamp); + nostr_signer_free(signer); + return evt; +} + int nostr_nip60_parse_quote_event(cJSON* event, const unsigned char* private_key, char* quote_id_out, diff --git a/nostr_core/nip060.h b/nostr_core/nip060.h index 1a00acee..3e38651d 100644 --- a/nostr_core/nip060.h +++ b/nostr_core/nip060.h @@ -11,6 +11,7 @@ #include #include "nip001.h" #include "nostr_common.h" +#include "nostr_signer.h" #include "../cjson/cJSON.h" #ifdef __cplusplus @@ -74,6 +75,9 @@ typedef struct { cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip60_parse_wallet_event(cJSON* event, const unsigned char* private_key, @@ -84,6 +88,9 @@ void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data); cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip60_parse_token_event(cJSON* event, const unsigned char* private_key, @@ -94,16 +101,27 @@ void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data); cJSON* nostr_nip60_create_token_deletion(const char* token_event_id, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id, + nostr_signer_t* signer, + time_t timestamp); cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs, const char** deleted_event_ids, int deleted_count, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs, + const char** deleted_event_ids, + int deleted_count, + nostr_signer_t* signer, + time_t timestamp); cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip60_parse_history_event(cJSON* event, const unsigned char* private_key, @@ -116,6 +134,11 @@ cJSON* nostr_nip60_create_quote_event(const char* quote_id, time_t expiration, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id, + const char* mint_url, + time_t expiration, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip60_parse_quote_event(cJSON* event, const unsigned char* private_key, diff --git a/nostr_core/nip061.c b/nostr_core/nip061.c index 67bab88f..43f21ec9 100644 --- a/nostr_core/nip061.c +++ b/nostr_core/nip061.c @@ -28,10 +28,10 @@ static int nip61_mint_in_info(const char* mint_url, const nostr_nip61_nutzap_inf return 0; } -cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info, - const unsigned char* private_key, - time_t timestamp) { - if (!info || !private_key || info->mint_count <= 0 || info->relay_count <= 0 || info->pubkey[0] == '\0') { +cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info, + nostr_signer_t* signer, + time_t timestamp) { + if (!info || !signer || info->mint_count <= 0 || info->relay_count <= 0 || info->pubkey[0] == '\0') { return NULL; } @@ -64,11 +64,27 @@ cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* inf cJSON_AddItemToArray(pub, cJSON_CreateString(info->pubkey)); cJSON_AddItemToArray(tags, pub); - cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, private_key, timestamp); + cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, signer, timestamp); cJSON_Delete(tags); return evt; } +cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) return NULL; + + signer = nostr_signer_local(private_key); + if (!signer) return NULL; + + evt = nostr_nip61_create_nutzap_info_event_with_signer(info, signer, timestamp); + nostr_signer_free(signer); + return evt; +} + int nostr_nip61_parse_nutzap_info_event(cJSON* event, nostr_nip61_nutzap_info_t* info_out) { if (!event || !info_out) return NOSTR_ERROR_INVALID_INPUT; @@ -204,10 +220,10 @@ void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info) { memset(info, 0, sizeof(*info)); } -cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data, - const unsigned char* sender_private_key, - time_t timestamp) { - if (!nutzap_data || !sender_private_key || !nutzap_data->mint_url || +cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data, + nostr_signer_t* signer, + time_t timestamp) { + if (!nutzap_data || !signer || !nutzap_data->mint_url || !nutzap_data->proofs || nutzap_data->proof_count <= 0 || nutzap_data->recipient_pubkey[0] == '\0') { return NULL; @@ -265,11 +281,27 @@ cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_d } const char* content = nutzap_data->content ? nutzap_data->content : ""; - cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP61_NUTZAP_KIND, content, tags, sender_private_key, timestamp); + cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP61_NUTZAP_KIND, content, tags, signer, timestamp); cJSON_Delete(tags); return evt; } +cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data, + const unsigned char* sender_private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!sender_private_key) return NULL; + + signer = nostr_signer_local(sender_private_key); + if (!signer) return NULL; + + evt = nostr_nip61_create_nutzap_event_with_signer(nutzap_data, signer, timestamp); + nostr_signer_free(signer); + return evt; +} + int nostr_nip61_parse_nutzap_event(cJSON* event, nostr_nip61_nutzap_data_t* nutzap_data_out) { if (!event || !nutzap_data_out) return NOSTR_ERROR_INVALID_INPUT; @@ -392,15 +424,15 @@ void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data) { memset(data, 0, sizeof(*data)); } -cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id, - const char* nutzap_relay_hint, - const char* sender_pubkey, - const char* created_token_event_id, - const char* created_token_relay_hint, - uint64_t amount, - const unsigned char* private_key, - time_t timestamp) { - if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !private_key) { +cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id, + const char* nutzap_relay_hint, + const char* sender_pubkey, + const char* created_token_event_id, + const char* created_token_relay_hint, + uint64_t amount, + nostr_signer_t* signer, + time_t timestamp) { + if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !signer) { return NULL; } @@ -419,7 +451,7 @@ cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id, hist.refs = refs; hist.ref_count = 1; - cJSON* evt = nostr_nip60_create_history_event(&hist, private_key, timestamp); + cJSON* evt = nostr_nip60_create_history_event_with_signer(&hist, signer, timestamp); if (!evt) return NULL; cJSON* tags = cJSON_GetObjectItem(evt, "tags"); @@ -440,6 +472,38 @@ cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id, return evt; } +cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id, + const char* nutzap_relay_hint, + const char* sender_pubkey, + const char* created_token_event_id, + const char* created_token_relay_hint, + uint64_t amount, + const unsigned char* private_key, + time_t timestamp) { + nostr_signer_t* signer; + cJSON* evt; + + if (!private_key) { + return NULL; + } + + signer = nostr_signer_local(private_key); + if (!signer) { + return NULL; + } + + evt = nostr_nip61_create_redemption_event_with_signer(nutzap_event_id, + nutzap_relay_hint, + sender_pubkey, + created_token_event_id, + created_token_relay_hint, + amount, + signer, + timestamp); + nostr_signer_free(signer); + return evt; +} + int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event) { if (!nutzap_event || !nutzap_info_event) return NOSTR_ERROR_INVALID_INPUT; diff --git a/nostr_core/nip061.h b/nostr_core/nip061.h index a1498bef..5bd53131 100644 --- a/nostr_core/nip061.h +++ b/nostr_core/nip061.h @@ -12,6 +12,7 @@ #include "nip001.h" #include "nip060.h" #include "nostr_common.h" +#include "nostr_signer.h" #include "../cjson/cJSON.h" #ifdef __cplusplus @@ -49,6 +50,9 @@ typedef struct { cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip61_parse_nutzap_info_event(cJSON* event, nostr_nip61_nutzap_info_t* info_out); @@ -58,6 +62,9 @@ void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info); cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data, const unsigned char* sender_private_key, time_t timestamp); +cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip61_parse_nutzap_event(cJSON* event, nostr_nip61_nutzap_data_t* nutzap_data_out); @@ -72,6 +79,14 @@ cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id, uint64_t amount, const unsigned char* private_key, time_t timestamp); +cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id, + const char* nutzap_relay_hint, + const char* sender_pubkey, + const char* created_token_event_id, + const char* created_token_relay_hint, + uint64_t amount, + nostr_signer_t* signer, + time_t timestamp); int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event); diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 19f3efdd..2e91606b 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.5.16" +#define VERSION "v0.6.6" #define VERSION_MAJOR 0 -#define VERSION_MINOR 5 -#define VERSION_PATCH 16 +#define VERSION_MINOR 6 +#define VERSION_PATCH 6 /* * NOSTR Core Library - Complete API Reference @@ -174,6 +174,7 @@ extern "C" { // Core common definitions and utilities #include "nostr_common.h" #include "utils.h" +#include "nostr_signer.h" // NIP implementations #include "nip001.h" // Basic Protocol @@ -261,6 +262,7 @@ nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void); int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url); int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url); int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable); +int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable); void nostr_relay_pool_destroy(nostr_relay_pool_t* pool); // Subscription management diff --git a/nostr_core/nostr_platform.h b/nostr_core/nostr_platform.h new file mode 100644 index 00000000..705c0866 --- /dev/null +++ b/nostr_core/nostr_platform.h @@ -0,0 +1,20 @@ +#ifndef NOSTR_PLATFORM_H +#define NOSTR_PLATFORM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Fill buffer with cryptographically secure random bytes. + * Returns 0 on success, -1 on failure. + */ +int nostr_platform_random(unsigned char *buf, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* NOSTR_PLATFORM_H */ diff --git a/nostr_core/nostr_signer.c b/nostr_core/nostr_signer.c new file mode 100644 index 00000000..2fb08476 --- /dev/null +++ b/nostr_core/nostr_signer.c @@ -0,0 +1,723 @@ +#include "nostr_signer.h" + +#include +#include +#include + +#include "utils.h" +#include "nip004.h" +#include "nip044.h" + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) +#include "nsigner_transport.h" +#include "nsigner_client.h" +#endif + +typedef enum { + NOSTR_SIGNER_BACKEND_LOCAL = 1, +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE = 2, +#endif +} nostr_signer_backend_t; + +struct nostr_signer { + nostr_signer_backend_t backend; + union { + struct { + unsigned char private_key[32]; + } local; +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + struct { + nsigner_client_t* client; + char role[64]; + } remote; +#endif + } u; +}; + +static int signer_hex_pubkey_to_bytes(const char* hex, unsigned char out[32]) { + if (!hex || strlen(hex) != 64 || !out) { + return NOSTR_ERROR_INVALID_INPUT; + } + if (nostr_hex_to_bytes(hex, out, 32) != 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + return NOSTR_SUCCESS; +} + +static int signer_local_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) { + unsigned char pubkey[32]; + + if (!signer || !out_pubkey_hex) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (nostr_ec_public_key_from_private_key(signer->u.local.private_key, pubkey) != 0) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + nostr_bytes_to_hex(pubkey, 32, out_pubkey_hex); + return NOSTR_SUCCESS; +} + +static int signer_local_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) { + cJSON* pubkey_item; + cJSON* created_at_item; + cJSON* kind_item; + cJSON* tags_item; + cJSON* content_item; + cJSON* serialize_array; + cJSON* signed_event = NULL; + char* serialize_string = NULL; + unsigned char event_hash[32]; + unsigned char signature[64]; + char event_id[65]; + char sig_hex[129]; + + if (!signer || !unsigned_event || !signed_event_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *signed_event_out = NULL; + + pubkey_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "pubkey"); + created_at_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "created_at"); + kind_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "kind"); + tags_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "tags"); + content_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "content"); + + if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) { + return NOSTR_ERROR_EVENT_INVALID_STRUCTURE; + } + + serialize_array = cJSON_CreateArray(); + if (!serialize_array) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0)); + cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1)); + cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1)); + cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1)); + cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1)); + cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1)); + + serialize_string = cJSON_PrintUnformatted(serialize_array); + cJSON_Delete(serialize_array); + + if (!serialize_string) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) { + free(serialize_string); + return NOSTR_ERROR_CRYPTO_FAILED; + } + + nostr_bytes_to_hex(event_hash, 32, event_id); + + if (nostr_ec_sign(signer->u.local.private_key, event_hash, signature) != 0) { + free(serialize_string); + return NOSTR_ERROR_CRYPTO_FAILED; + } + + nostr_bytes_to_hex(signature, 64, sig_hex); + + signed_event = cJSON_Duplicate((cJSON*)unsigned_event, 1); + if (!signed_event) { + free(serialize_string); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_DeleteItemFromObjectCaseSensitive(signed_event, "id"); + cJSON_DeleteItemFromObjectCaseSensitive(signed_event, "sig"); + cJSON_AddStringToObject(signed_event, "id", event_id); + cJSON_AddStringToObject(signed_event, "sig", sig_hex); + + *signed_event_out = signed_event; + free(serialize_string); + + return NOSTR_SUCCESS; +} + +static int signer_local_nip04_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out) { + unsigned char peer_pubkey[32]; + char* out; + int rc; + + if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *ciphertext_out = NULL; + + rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + out = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE + 1); + if (!out) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nostr_nip04_encrypt(signer->u.local.private_key, peer_pubkey, plaintext, out, NOSTR_NIP04_MAX_ENCRYPTED_SIZE + 1); + if (rc != NOSTR_SUCCESS) { + free(out); + return rc; + } + + *ciphertext_out = out; + return NOSTR_SUCCESS; +} + +static int signer_local_nip04_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out) { + unsigned char peer_pubkey[32]; + char* out; + int rc; + + if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *plaintext_out = NULL; + + rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + out = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1); + if (!out) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nostr_nip04_decrypt(signer->u.local.private_key, peer_pubkey, ciphertext, out, NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1); + if (rc != NOSTR_SUCCESS) { + free(out); + return rc; + } + + *plaintext_out = out; + return NOSTR_SUCCESS; +} + +static int signer_local_nip44_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out) { + unsigned char peer_pubkey[32]; + char* out; + size_t out_size; + int rc; + + if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *ciphertext_out = NULL; + + rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + out_size = (strlen(plaintext) * 4) + 4096; + if (out_size < 8192) { + out_size = 8192; + } + + out = (char*)malloc(out_size); + if (!out) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nostr_nip44_encrypt(signer->u.local.private_key, peer_pubkey, plaintext, out, out_size); + if (rc != NOSTR_SUCCESS) { + free(out); + return rc; + } + + *ciphertext_out = out; + return NOSTR_SUCCESS; +} + +static int signer_local_nip44_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out) { + unsigned char peer_pubkey[32]; + char* out; + int rc; + + if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *plaintext_out = NULL; + + rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + out = (char*)malloc(NOSTR_NIP44_MAX_PLAINTEXT_SIZE + 1); + if (!out) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nostr_nip44_decrypt(signer->u.local.private_key, peer_pubkey, ciphertext, out, NOSTR_NIP44_MAX_PLAINTEXT_SIZE + 1); + if (rc != NOSTR_SUCCESS) { + free(out); + return rc; + } + + *plaintext_out = out; + return NOSTR_SUCCESS; +} + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + +static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role) { + cJSON* selector; + + if (params == NULL) { + params = cJSON_CreateArray(); + if (params == NULL) { + return NULL; + } + } + + if (role == NULL || role[0] == '\0') { + return params; + } + + selector = cJSON_CreateObject(); + if (selector == NULL) { + cJSON_Delete(params); + return NULL; + } + + cJSON_AddStringToObject(selector, "role", role); + cJSON_AddItemToArray(params, selector); + return params; +} + +static int signer_remote_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) { + cJSON* params; + cJSON* result = NULL; + const char* result_str; + int rc; + + if (!signer || !out_pubkey_hex) { + return NOSTR_ERROR_INVALID_INPUT; + } + + params = signer_remote_params_with_selector(NULL, signer->u.remote.role); + if (params == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nsigner_client_call(signer->u.remote.client, "nostr_get_public_key", params, &result); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + if (!cJSON_IsString(result) || result->valuestring == NULL || strlen(result->valuestring) != 64) { + cJSON_Delete(result); + return NOSTR_ERROR_NIP46_INVALID_RESPONSE; + } + + result_str = result->valuestring; + memcpy(out_pubkey_hex, result_str, 64); + out_pubkey_hex[64] = '\0'; + + cJSON_Delete(result); + return NOSTR_SUCCESS; +} + +static int signer_remote_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) { + cJSON* params; + cJSON* result = NULL; + cJSON* parsed = NULL; + char* event_json; + int rc; + + if (!signer || !unsigned_event || !signed_event_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *signed_event_out = NULL; + + event_json = cJSON_PrintUnformatted((cJSON*)unsigned_event); + if (event_json == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + params = cJSON_CreateArray(); + if (params == NULL) { + free(event_json); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddItemToArray(params, cJSON_CreateString(event_json)); + free(event_json); + + params = signer_remote_params_with_selector(params, signer->u.remote.role); + if (params == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nsigner_client_call(signer->u.remote.client, "nostr_sign_event", params, &result); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + if (cJSON_IsString(result) && result->valuestring != NULL) { + parsed = cJSON_Parse(result->valuestring); + cJSON_Delete(result); + if (parsed == NULL || !cJSON_IsObject(parsed)) { + cJSON_Delete(parsed); + return NOSTR_ERROR_NIP46_INVALID_RESPONSE; + } + *signed_event_out = parsed; + return NOSTR_SUCCESS; + } + + if (cJSON_IsObject(result)) { + *signed_event_out = cJSON_Duplicate(result, 1); + cJSON_Delete(result); + if (*signed_event_out == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + return NOSTR_SUCCESS; + } + + cJSON_Delete(result); + return NOSTR_ERROR_NIP46_INVALID_RESPONSE; +} + +static int signer_remote_encrypt_decrypt(nostr_signer_t* signer, + const char* method, + const char* peer_pubkey_hex, + const char* in, + char** out) { + cJSON* params; + cJSON* result = NULL; + const char* result_str; + int rc; + + if (!signer || !method || !peer_pubkey_hex || !in || !out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *out = NULL; + + params = cJSON_CreateArray(); + if (params == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey_hex)); + cJSON_AddItemToArray(params, cJSON_CreateString(in)); + + params = signer_remote_params_with_selector(params, signer->u.remote.role); + if (params == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = nsigner_client_call(signer->u.remote.client, method, params, &result); + if (rc != NOSTR_SUCCESS) { + return rc; + } + + if (!cJSON_IsString(result) || result->valuestring == NULL) { + cJSON_Delete(result); + return NOSTR_ERROR_NIP46_INVALID_RESPONSE; + } + + result_str = result->valuestring; + { + size_t n = strlen(result_str); + *out = (char*)malloc(n + 1); + if (*out == NULL) { + cJSON_Delete(result); + return NOSTR_ERROR_MEMORY_FAILED; + } + memcpy(*out, result_str, n + 1); + } + cJSON_Delete(result); + + return NOSTR_SUCCESS; +} + +static int signer_remote_nip04_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out) { + return signer_remote_encrypt_decrypt(signer, "nostr_nip04_encrypt", peer_pubkey_hex, plaintext, ciphertext_out); +} + +static int signer_remote_nip04_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out) { + return signer_remote_encrypt_decrypt(signer, "nostr_nip04_decrypt", peer_pubkey_hex, ciphertext, plaintext_out); +} + +static int signer_remote_nip44_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out) { + return signer_remote_encrypt_decrypt(signer, "nostr_nip44_encrypt", peer_pubkey_hex, plaintext, ciphertext_out); +} + +static int signer_remote_nip44_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out) { + return signer_remote_encrypt_decrypt(signer, "nostr_nip44_decrypt", peer_pubkey_hex, ciphertext, plaintext_out); +} + +#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */ + +nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]) { + nostr_signer_t* signer; + + if (!private_key) { + return NULL; + } + + signer = (nostr_signer_t*)calloc(1, sizeof(nostr_signer_t)); + if (!signer) { + return NULL; + } + + signer->backend = NOSTR_SIGNER_BACKEND_LOCAL; + memcpy(signer->u.local.private_key, private_key, 32); + return signer; +} + +void nostr_signer_free(nostr_signer_t* signer) { + if (!signer) { + return; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + memset(signer->u.local.private_key, 0, sizeof(signer->u.local.private_key)); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + else if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + if (signer->u.remote.client != NULL) { + nsigner_client_free(signer->u.remote.client); + signer->u.remote.client = NULL; + } + memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role)); + } +#endif + + free(signer); +} + +int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) { + if (!signer || !out_pubkey_hex) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + return signer_local_get_public_key(signer, out_pubkey_hex); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + return signer_remote_get_public_key(signer, out_pubkey_hex); + } +#endif + + return NOSTR_ERROR_INVALID_INPUT; +} + +int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) { + if (!signer || !unsigned_event || !signed_event_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + return signer_local_sign_event(signer, unsigned_event, signed_event_out); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + return signer_remote_sign_event(signer, unsigned_event, signed_event_out); + } +#endif + + return NOSTR_ERROR_INVALID_INPUT; +} + +int nostr_signer_nip04_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out) { + if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + return signer_local_nip04_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + return signer_remote_nip04_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out); + } +#endif + + return NOSTR_ERROR_INVALID_INPUT; +} + +int nostr_signer_nip04_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out) { + if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + return signer_local_nip04_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + return signer_remote_nip04_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out); + } +#endif + + return NOSTR_ERROR_INVALID_INPUT; +} + +int nostr_signer_nip44_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out) { + if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + return signer_local_nip44_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + return signer_remote_nip44_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out); + } +#endif + + return NOSTR_ERROR_INVALID_INPUT; +} + +int nostr_signer_nip44_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out) { + if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) { + return signer_local_nip44_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out); + } +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) { + return signer_remote_nip44_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out); + } +#endif + + return NOSTR_ERROR_INVALID_INPUT; +} + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) +static nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role) { + nsigner_client_t* client = NULL; + nostr_signer_t* signer = NULL; + + if (transport == NULL) { + return NULL; + } + + client = nsigner_client_new(transport); + if (client == NULL) { + return NULL; + } + + signer = (nostr_signer_t*)calloc(1, sizeof(*signer)); + if (signer == NULL) { + nsigner_client_free(client); + return NULL; + } + + signer->backend = NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE; + signer->u.remote.client = client; + if (role != NULL) { + strncpy(signer->u.remote.role, role, sizeof(signer->u.remote.role) - 1); + signer->u.remote.role[sizeof(signer->u.remote.role) - 1] = '\0'; + } + + return signer; +} + +nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms) { + nsigner_transport_t* transport = NULL; + char discovered[8][64]; + const char* effective_name = socket_name; + + if (effective_name == NULL || effective_name[0] == '\0') { + int count = nsigner_transport_list_unix(discovered, 8); + if (count != 1) { + return NULL; + } + effective_name = discovered[0]; + } + + transport = nsigner_transport_open_unix(effective_name, timeout_ms); + return nostr_signer_nsigner_from_transport(transport, role); +} + +nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms) { + nsigner_transport_t* transport; + + transport = nsigner_transport_open_serial(device_path, timeout_ms); + return nostr_signer_nsigner_from_transport(transport, role); +} + +nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms) { + nsigner_transport_t* transport; + + transport = nsigner_transport_open_tcp(host, port, timeout_ms); + return nostr_signer_nsigner_from_transport(transport, role); +} + +nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms) { + nsigner_transport_t* transport; + + transport = nsigner_transport_open_fds(read_fd, write_fd, timeout_ms); + return nostr_signer_nsigner_from_transport(transport, role); +} + +int nostr_signer_nsigner_set_auth(nostr_signer_t* signer, + const unsigned char auth_privkey[32], + const char* label) { + if (signer == NULL || auth_privkey == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend != NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE || signer->u.remote.client == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + return nsigner_client_set_auth(signer->u.remote.client, auth_privkey, label); +} +#endif diff --git a/nostr_core/nostr_signer.h b/nostr_core/nostr_signer.h new file mode 100644 index 00000000..fc255f7c --- /dev/null +++ b/nostr_core/nostr_signer.h @@ -0,0 +1,55 @@ +#ifndef NOSTR_SIGNER_H +#define NOSTR_SIGNER_H + +#include +#include "nostr_common.h" +#include "../cjson/cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct nostr_signer nostr_signer_t; + +/* Lifecycle */ +nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]); +void nostr_signer_free(nostr_signer_t* signer); + +/* Core verbs */ +int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]); +int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out); + +/* Encryption verbs (for parity with upcoming remote backends) */ +int nostr_signer_nip04_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out); +int nostr_signer_nip04_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out); +int nostr_signer_nip44_encrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* plaintext, + char** ciphertext_out); +int nostr_signer_nip44_decrypt(nostr_signer_t* signer, + const char* peer_pubkey_hex, + const char* ciphertext, + char** plaintext_out); + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) +nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms); +/* TCP mode requires auth envelope per n_signer; set this before making calls. */ +int nostr_signer_nsigner_set_auth(nostr_signer_t* signer, + const unsigned char auth_privkey[32], + const char* label); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* NOSTR_SIGNER_H */ diff --git a/nostr_core/nsigner_client.c b/nostr_core/nsigner_client.c new file mode 100644 index 00000000..34e36aa9 --- /dev/null +++ b/nostr_core/nsigner_client.c @@ -0,0 +1,383 @@ +#define _POSIX_C_SOURCE 200112L + +#include "nsigner_client.h" + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + +#include +#include +#include +#include +#include + +#include "nip001.h" +#include "utils.h" + +/* Ensure a broken connection returns EPIPE instead of killing the process. */ +static void nsigner_client_ignore_sigpipe_once(void) { + static int done = 0; + if (!done) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + (void)sigaction(SIGPIPE, &sa, NULL); + done = 1; + } +} + +struct nsigner_client { + nsigner_transport_t* transport; + unsigned long long next_id; + int auth_enabled; + int connected; /* whether the transport has an open connection ready for a request */ + unsigned char auth_privkey[32]; + char auth_label[64]; + char last_error[256]; +}; + +static void nsigner_client_set_error(nsigner_client_t* client, const char* msg) { + if (client == NULL) { + return; + } + if (msg == NULL) { + msg = "unknown"; + } + strncpy(client->last_error, msg, sizeof(client->last_error) - 1); + client->last_error[sizeof(client->last_error) - 1] = '\0'; +} + +int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]) { + unsigned char hash[32]; + char* compact; + + if (out_hash_hex == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (params == NULL) { + compact = (char*)malloc(5); + if (compact == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + memcpy(compact, "null", 5); + } else { + compact = cJSON_PrintUnformatted((cJSON*)params); + if (compact == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + } + + if (nostr_sha256((const unsigned char*)compact, strlen(compact), hash) != 0) { + free(compact); + return NOSTR_ERROR_CRYPTO_FAILED; + } + + nostr_bytes_to_hex(hash, sizeof(hash), out_hash_hex); + out_hash_hex[64] = '\0'; + free(compact); + return NOSTR_SUCCESS; +} + +static cJSON* nsigner_client_make_tag_pair(const char* key, const char* value) { + cJSON* pair; + + if (key == NULL || value == NULL) { + return NULL; + } + + pair = cJSON_CreateArray(); + if (pair == NULL) { + return NULL; + } + + cJSON_AddItemToArray(pair, cJSON_CreateString(key)); + cJSON_AddItemToArray(pair, cJSON_CreateString(value)); + return pair; +} + +static int nsigner_client_build_auth(nsigner_client_t* client, + const char* request_id, + const char* method, + const cJSON* params, + cJSON** out_auth) { + cJSON* tags = NULL; + cJSON* auth = NULL; + char hash_hex[65]; + + if (client == NULL || request_id == NULL || method == NULL || out_auth == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *out_auth = NULL; + + if (nsigner_client_compute_body_hash_hex(params, hash_hex) != NOSTR_SUCCESS) { + return NOSTR_ERROR_CRYPTO_FAILED; + } + + tags = cJSON_CreateArray(); + if (tags == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_rpc", request_id)); + cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_method", method)); + cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_body_hash", hash_hex)); + + auth = nostr_create_and_sign_event(27235, + client->auth_label, + tags, + client->auth_privkey, + time(NULL)); + if (auth == NULL) { + cJSON_Delete(tags); + return NOSTR_ERROR_CRYPTO_FAILED; + } + + *out_auth = auth; + return NOSTR_SUCCESS; +} + +static int nsigner_client_next_id(nsigner_client_t* client, char out_id[32]) { + if (client == NULL || out_id == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + snprintf(out_id, 32, "%llu", client->next_id); + client->next_id++; + return NOSTR_SUCCESS; +} + +static int nsigner_client_map_rpc_error(int rpc_code, const char* rpc_message) { + if (rpc_code == 2010 || rpc_code == 2011 || rpc_code == 2012 || rpc_code == 2013 || + rpc_code == 2014 || rpc_code == 2015 || rpc_code == 2016 || rpc_code == 2017) { + return NOSTR_ERROR_NIP46_AUTH_CHALLENGE; + } + + if (rpc_message != NULL) { + if (strcmp(rpc_message, "approval_denied") == 0 || strcmp(rpc_message, "unauthorized") == 0) { + return NOSTR_ERROR_AUTH_RULES_DENIED; + } + if (strcmp(rpc_message, "method_not_found") == 0) { + return NOSTR_ERROR_NIP46_UNKNOWN_METHOD; + } + if (strcmp(rpc_message, "invalid_request") == 0) { + return NOSTR_ERROR_NIP46_INVALID_REQUEST; + } + if (strcmp(rpc_message, "ambiguous_role_selector") == 0 || strcmp(rpc_message, "unknown_role") == 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + } + + if (rpc_code >= 1000 && rpc_code < 2000) { + return NOSTR_ERROR_NIP46_INVALID_REQUEST; + } + return NOSTR_ERROR_NETWORK_FAILED; +} + +nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport) { + nsigner_client_t* client; + + if (transport == NULL) { + return NULL; + } + + client = (nsigner_client_t*)calloc(1, sizeof(*client)); + if (client == NULL) { + transport->close(transport); + return NULL; + } + + client->transport = transport; + client->next_id = 1; + client->connected = 1; /* transport was opened connected by its constructor */ + nsigner_client_set_error(client, "ok"); + return client; +} + +void nsigner_client_free(nsigner_client_t* client) { + if (client == NULL) { + return; + } + + if (client->transport != NULL) { + client->transport->close(client->transport); + client->transport = NULL; + } + + memset(client->auth_privkey, 0, sizeof(client->auth_privkey)); + memset(client->auth_label, 0, sizeof(client->auth_label)); + free(client); +} + +int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label) { + if (client == NULL || privkey == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + memcpy(client->auth_privkey, privkey, 32); + client->auth_enabled = 1; + + if (label == NULL) { + label = ""; + } + + strncpy(client->auth_label, label, sizeof(client->auth_label) - 1); + client->auth_label[sizeof(client->auth_label) - 1] = '\0'; + return NOSTR_SUCCESS; +} + +int nsigner_client_call(nsigner_client_t* client, + const char* method, + cJSON* params, + cJSON** out_result) { + cJSON* req = NULL; + cJSON* res = NULL; + cJSON* result_item; + cJSON* error_item; + cJSON* code_item; + cJSON* message_item; + cJSON* params_item; + cJSON* auth = NULL; + char id_buf[32]; + char* req_json = NULL; + char* res_json = NULL; + size_t res_len = 0; + int rc; + + if (client == NULL || method == NULL || out_result == NULL || client->transport == NULL) { + cJSON_Delete(params); + return NOSTR_ERROR_INVALID_INPUT; + } + + *out_result = NULL; + + nsigner_client_ignore_sigpipe_once(); + + /* + * The n_signer server closes the connection after handling a single framed + * request (one-request-per-connection model). Reconnect before every call + * except the first (the transport was opened connected by the constructor). + * Transports that do not support reconnection (fd-pair) skip this. + */ + if (client->connected && client->transport->reconnect != NULL) { + if (client->transport->reconnect(client->transport) != 0) { + nsigner_client_set_error(client, "transport reconnect failed"); + cJSON_Delete(params); + return NOSTR_ERROR_IO_FAILED; + } + } + client->connected = 1; + + rc = nsigner_client_next_id(client, id_buf); + if (rc != NOSTR_SUCCESS) { + cJSON_Delete(params); + return rc; + } + + req = cJSON_CreateObject(); + if (req == NULL) { + cJSON_Delete(params); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddStringToObject(req, "id", id_buf); + cJSON_AddStringToObject(req, "method", method); + + if (params == NULL) { + params_item = cJSON_CreateNull(); + } else { + params_item = params; + params = NULL; + } + + if (params_item == NULL) { + cJSON_Delete(req); + cJSON_Delete(params); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddItemToObject(req, "params", params_item); + + if (client->auth_enabled) { + rc = nsigner_client_build_auth(client, id_buf, method, params_item, &auth); + if (rc != NOSTR_SUCCESS) { + nsigner_client_set_error(client, "failed to build auth envelope"); + cJSON_Delete(req); + return rc; + } + cJSON_AddItemToObject(req, "auth", auth); + auth = NULL; + } + + req_json = cJSON_PrintUnformatted(req); + cJSON_Delete(req); + if (req_json == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + rc = client->transport->send_framed(client->transport, req_json, strlen(req_json)); + free(req_json); + if (rc != 0) { + nsigner_client_set_error(client, "transport send failed"); + return NOSTR_ERROR_IO_FAILED; + } + + rc = client->transport->recv_framed(client->transport, &res_json, &res_len); + if (rc != 0 || res_json == NULL || res_len == 0) { + free(res_json); + nsigner_client_set_error(client, "transport receive failed"); + return NOSTR_ERROR_IO_FAILED; + } + + res = cJSON_Parse(res_json); + free(res_json); + if (res == NULL) { + nsigner_client_set_error(client, "invalid JSON response"); + return NOSTR_ERROR_NIP46_INVALID_RESPONSE; + } + + error_item = cJSON_GetObjectItemCaseSensitive(res, "error"); + if (cJSON_IsObject(error_item)) { + int rpc_code = 0; + const char* rpc_msg = "rpc_error"; + + code_item = cJSON_GetObjectItemCaseSensitive(error_item, "code"); + message_item = cJSON_GetObjectItemCaseSensitive(error_item, "message"); + if (cJSON_IsNumber(code_item)) { + rpc_code = code_item->valueint; + } + if (cJSON_IsString(message_item) && message_item->valuestring != NULL) { + rpc_msg = message_item->valuestring; + } + + nsigner_client_set_error(client, rpc_msg); + cJSON_Delete(res); + return nsigner_client_map_rpc_error(rpc_code, rpc_msg); + } + + result_item = cJSON_GetObjectItemCaseSensitive(res, "result"); + if (result_item == NULL) { + nsigner_client_set_error(client, "missing result field"); + cJSON_Delete(res); + return NOSTR_ERROR_NIP46_INVALID_RESPONSE; + } + + *out_result = cJSON_Duplicate(result_item, 1); + cJSON_Delete(res); + + if (*out_result == NULL) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + nsigner_client_set_error(client, "ok"); + return NOSTR_SUCCESS; +} + +const char* nsigner_client_last_error(const nsigner_client_t* client) { + if (client == NULL) { + return "invalid_client"; + } + return client->last_error; +} + +#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */ diff --git a/nostr_core/nsigner_client.h b/nostr_core/nsigner_client.h new file mode 100644 index 00000000..35ace4ad --- /dev/null +++ b/nostr_core/nsigner_client.h @@ -0,0 +1,44 @@ +#ifndef NSIGNER_CLIENT_H +#define NSIGNER_CLIENT_H + +#include +#include + +#include "nostr_common.h" +#include "nsigner_transport.h" +#include "../cjson/cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + +typedef struct nsigner_client nsigner_client_t; + +nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport); /* takes ownership of transport */ +void nsigner_client_free(nsigner_client_t* client); + +int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label); + +/* + * low-level: build request JSON, attach auth envelope if enabled, send, recv, parse result/error + * params may be NULL and ownership is transferred to the call. + */ +int nsigner_client_call(nsigner_client_t* client, + const char* method, + cJSON* params, + cJSON** out_result); + +const char* nsigner_client_last_error(const nsigner_client_t* client); + +/* Exposed for offline tests and parity checks with n_signer hashing behavior. */ +int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]); + +#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */ + +#ifdef __cplusplus +} +#endif + +#endif /* NSIGNER_CLIENT_H */ diff --git a/nostr_core/nsigner_transport.c b/nostr_core/nsigner_transport.c new file mode 100644 index 00000000..3a65c91a --- /dev/null +++ b/nostr_core/nsigner_transport.c @@ -0,0 +1,869 @@ +#define _POSIX_C_SOURCE 200112L + +#include "nsigner_transport.h" + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NSIGNER_CLIENT_MAX_FRAME 65536U + +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif + +#ifndef CRTSCTS +#define CRTSCTS 0 +#endif + +typedef enum { + NSIGNER_TRANSPORT_KIND_FD = 0, + NSIGNER_TRANSPORT_KIND_UNIX = 1, + NSIGNER_TRANSPORT_KIND_TCP = 2, + NSIGNER_TRANSPORT_KIND_SERIAL = 3 +} nsigner_transport_kind_t; + +/* Forward declarations: reconnect helpers use these before their definitions. */ +static void nsigner_termios_make_raw(struct termios* tio); +static int nsigner_set_socket_timeouts(int fd, int timeout_ms); +static int nsigner_connect_with_timeout(int fd, const struct sockaddr* addr, socklen_t addr_len, int timeout_ms); + +typedef struct { + nsigner_transport_kind_t kind; + int read_fd; + int write_fd; + int timeout_ms; + int close_read_fd; + int close_write_fd; + /* Reconnection parameters (used by unix/tcp/serial). */ + char socket_name[108]; /* unix: abstract name without leading '@' */ + char host[256]; /* tcp: host */ + int port; /* tcp: port */ + char device_path[256]; /* serial: device path */ +} nsigner_fd_transport_ctx_t; + +static int nsigner_write_full(int fd, const void* buf, size_t len) { + const unsigned char* p = (const unsigned char*)buf; + size_t off = 0; + + while (off < len) { + ssize_t n = write(fd, p + off, len - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (n == 0) { + return -1; + } + off += (size_t)n; + } + + return 0; +} + +static int nsigner_wait_readable(int fd, int timeout_ms) { + struct pollfd pfd; + int rc; + + if (timeout_ms < 0) { + return 0; + } + + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + + do { + rc = poll(&pfd, 1, timeout_ms); + } while (rc < 0 && errno == EINTR); + + if (rc <= 0) { + return -1; + } + + if ((pfd.revents & (POLLIN | POLLERR | POLLHUP)) == 0) { + return -1; + } + + return 0; +} + +static int nsigner_read_full_timeout(int fd, void* buf, size_t len, int timeout_ms) { + unsigned char* p = (unsigned char*)buf; + size_t off = 0; + + while (off < len) { + ssize_t n; + + if (nsigner_wait_readable(fd, timeout_ms) != 0) { + return -1; + } + + n = read(fd, p + off, len - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; + } + return -1; + } + if (n == 0) { + return -1; + } + off += (size_t)n; + } + + return 0; +} + +int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len) { + uint32_t be_len; + + if (fd < 0 || json == NULL || len == 0 || len > NSIGNER_CLIENT_MAX_FRAME) { + return -1; + } + + be_len = htonl((uint32_t)len); + if (nsigner_write_full(fd, &be_len, sizeof(be_len)) != 0) { + return -1; + } + if (nsigner_write_full(fd, json, len) != 0) { + return -1; + } + + return 0; +} + +static int nsigner_transport_recv_framed_fd_timeout(int fd, char** out_json, size_t* out_len, int timeout_ms) { + uint32_t be_len; + uint32_t payload_len; + char* payload; + + if (fd < 0 || out_json == NULL || out_len == NULL) { + return -1; + } + + *out_json = NULL; + *out_len = 0; + + if (nsigner_read_full_timeout(fd, &be_len, sizeof(be_len), timeout_ms) != 0) { + return -1; + } + + payload_len = ntohl(be_len); + if (payload_len == 0 || payload_len > NSIGNER_CLIENT_MAX_FRAME) { + return -1; + } + + payload = (char*)malloc((size_t)payload_len + 1U); + if (payload == NULL) { + return -1; + } + + if (nsigner_read_full_timeout(fd, payload, (size_t)payload_len, timeout_ms) != 0) { + free(payload); + return -1; + } + + payload[payload_len] = '\0'; + *out_json = payload; + *out_len = (size_t)payload_len; + return 0; +} + +int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len) { + return nsigner_transport_recv_framed_fd_timeout(fd, out_json, out_len, -1); +} + +static int nsigner_fd_send_framed(nsigner_transport_t* t, const char* json, size_t len) { + nsigner_fd_transport_ctx_t* ctx; + + if (t == NULL || t->ctx == NULL) { + return -1; + } + + ctx = (nsigner_fd_transport_ctx_t*)t->ctx; + return nsigner_transport_send_framed_fd(ctx->write_fd, json, len); +} + +static int nsigner_fd_recv_framed(nsigner_transport_t* t, char** out_json, size_t* out_len) { + nsigner_fd_transport_ctx_t* ctx; + + if (t == NULL || t->ctx == NULL) { + return -1; + } + + ctx = (nsigner_fd_transport_ctx_t*)t->ctx; + return nsigner_transport_recv_framed_fd_timeout(ctx->read_fd, out_json, out_len, ctx->timeout_ms); +} + +static void nsigner_fd_close_fds(nsigner_fd_transport_ctx_t* ctx) { + if (ctx == NULL) { + return; + } + + if (ctx->close_read_fd && ctx->read_fd >= 0) { + int read_fd = ctx->read_fd; + close(read_fd); + ctx->read_fd = -1; + if (ctx->write_fd == read_fd) { + ctx->write_fd = -1; + } + } + if (ctx->close_write_fd && ctx->write_fd >= 0) { + close(ctx->write_fd); + ctx->write_fd = -1; + } +} + +static int nsigner_fd_reconnect_unix(nsigner_fd_transport_ctx_t* ctx) { + struct sockaddr_un addr; + size_t name_len; + socklen_t addr_len; + int fd; + + name_len = strlen(ctx->socket_name); + if (name_len > (sizeof(addr.sun_path) - 2)) { + return -1; + } + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (nsigner_set_socket_timeouts(fd, ctx->timeout_ms) != 0) { + close(fd); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + addr.sun_path[0] = '\0'; + memcpy(&addr.sun_path[1], ctx->socket_name, name_len); + + addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + name_len); + if (nsigner_connect_with_timeout(fd, (struct sockaddr*)&addr, addr_len, ctx->timeout_ms) != 0) { + close(fd); + return -1; + } + + ctx->read_fd = fd; + ctx->write_fd = fd; + return 0; +} + +static int nsigner_fd_reconnect_tcp(nsigner_fd_transport_ctx_t* ctx) { + struct addrinfo hints; + struct addrinfo* addrs = NULL; + struct addrinfo* ai; + char port_buf[16]; + int fd = -1; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + snprintf(port_buf, sizeof(port_buf), "%d", ctx->port); + if (getaddrinfo(ctx->host, port_buf, &hints, &addrs) != 0) { + return -1; + } + + for (ai = addrs; ai != NULL; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) { + continue; + } + + if (nsigner_connect_with_timeout(fd, ai->ai_addr, ai->ai_addrlen, ctx->timeout_ms) == 0 && + nsigner_set_socket_timeouts(fd, ctx->timeout_ms) == 0) { + break; + } + + close(fd); + fd = -1; + } + + freeaddrinfo(addrs); + + if (fd < 0) { + return -1; + } + + ctx->read_fd = fd; + ctx->write_fd = fd; + return 0; +} + +static int nsigner_fd_reconnect_serial(nsigner_fd_transport_ctx_t* ctx) { + struct termios tio; + int fd; + + fd = open(ctx->device_path, O_RDWR | O_NOCTTY | O_CLOEXEC); + if (fd < 0) { + return -1; + } + + if (tcgetattr(fd, &tio) != 0) { + close(fd); + return -1; + } + + nsigner_termios_make_raw(&tio); + tio.c_cflag |= (CLOCAL | CREAD); + tio.c_cflag &= ~(CRTSCTS | CSTOPB | PARENB); + tio.c_cflag = (tio.c_cflag & ~CSIZE) | CS8; + tio.c_iflag &= ~(IXON | IXOFF | IXANY); + tio.c_cc[VMIN] = 0; + tio.c_cc[VTIME] = 0; + + (void)cfsetispeed(&tio, B115200); + (void)cfsetospeed(&tio, B115200); + + if (tcsetattr(fd, TCSANOW, &tio) != 0) { + close(fd); + return -1; + } + + ctx->read_fd = fd; + ctx->write_fd = fd; + return 0; +} + +static int nsigner_fd_reconnect(nsigner_transport_t* t) { + nsigner_fd_transport_ctx_t* ctx; + int rc; + + if (t == NULL || t->ctx == NULL) { + return -1; + } + + ctx = (nsigner_fd_transport_ctx_t*)t->ctx; + + /* Close any existing fds first. */ + nsigner_fd_close_fds(ctx); + + switch (ctx->kind) { + case NSIGNER_TRANSPORT_KIND_UNIX: + rc = nsigner_fd_reconnect_unix(ctx); + break; + case NSIGNER_TRANSPORT_KIND_TCP: + rc = nsigner_fd_reconnect_tcp(ctx); + break; + case NSIGNER_TRANSPORT_KIND_SERIAL: + rc = nsigner_fd_reconnect_serial(ctx); + break; + default: + /* FD-pair transport does not support reconnection. */ + return -1; + } + + return rc; +} + +static void nsigner_fd_close(nsigner_transport_t* t) { + nsigner_fd_transport_ctx_t* ctx; + + if (t == NULL) { + return; + } + + ctx = (nsigner_fd_transport_ctx_t*)t->ctx; + if (ctx != NULL) { + nsigner_fd_close_fds(ctx); + free(ctx); + } + + t->ctx = NULL; + free(t); +} + +static int nsigner_fd_ctx_init(nsigner_transport_t* t, + nsigner_fd_transport_ctx_t* ctx, + int read_fd, + int write_fd, + int timeout_ms, + int close_read_fd, + int close_write_fd) { + if (t == NULL || ctx == NULL || read_fd < 0 || write_fd < 0) { + return -1; + } + + ctx->kind = NSIGNER_TRANSPORT_KIND_FD; + ctx->read_fd = read_fd; + ctx->write_fd = write_fd; + ctx->timeout_ms = timeout_ms; + ctx->close_read_fd = close_read_fd; + ctx->close_write_fd = close_write_fd; + + t->ctx = ctx; + t->send_framed = nsigner_fd_send_framed; + t->recv_framed = nsigner_fd_recv_framed; + t->reconnect = NULL; /* fd-pair transport does not support reconnection */ + t->close = nsigner_fd_close; + return 0; +} + +static int nsigner_set_socket_timeouts(int fd, int timeout_ms) { + struct timeval tv; + + if (fd < 0) { + return -1; + } + + if (timeout_ms <= 0) { + timeout_ms = 5000; + } + + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + + if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) != 0) { + return -1; + } + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) { + return -1; + } + + return 0; +} + +static int nsigner_connect_with_timeout(int fd, const struct sockaddr* addr, socklen_t addr_len, int timeout_ms) { + int flags; + int rc; + + if (fd < 0 || addr == NULL) { + return -1; + } + + if (timeout_ms <= 0) { + timeout_ms = 5000; + } + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { + return -1; + } + + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + return -1; + } + + rc = connect(fd, addr, addr_len); + if (rc != 0) { + if (errno != EINPROGRESS) { + (void)fcntl(fd, F_SETFL, flags); + return -1; + } + + { + struct pollfd pfd; + int prc; + int so_error = 0; + socklen_t so_error_len = (socklen_t)sizeof(so_error); + + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + + do { + prc = poll(&pfd, 1, timeout_ms); + } while (prc < 0 && errno == EINTR); + + if (prc <= 0) { + (void)fcntl(fd, F_SETFL, flags); + return -1; + } + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_error_len) != 0 || so_error != 0) { + (void)fcntl(fd, F_SETFL, flags); + if (so_error != 0) { + errno = so_error; + } + return -1; + } + } + } + + if (fcntl(fd, F_SETFL, flags) != 0) { + return -1; + } + + return 0; +} + +nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms) { + nsigner_transport_t* t = NULL; + nsigner_fd_transport_ctx_t* ctx = NULL; + struct sockaddr_un addr; + size_t name_len; + socklen_t addr_len; + int fd; + + if (socket_name == NULL || socket_name[0] == '\0') { + return NULL; + } + + name_len = strlen(socket_name); + if (name_len > (sizeof(addr.sun_path) - 2)) { + return NULL; + } + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + return NULL; + } + + if (timeout_ms <= 0) { + timeout_ms = 5000; + } + + if (nsigner_set_socket_timeouts(fd, timeout_ms) != 0) { + close(fd); + return NULL; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + addr.sun_path[0] = '\0'; + memcpy(&addr.sun_path[1], socket_name, name_len); + + addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + name_len); + if (nsigner_connect_with_timeout(fd, (struct sockaddr*)&addr, addr_len, timeout_ms) != 0) { + close(fd); + return NULL; + } + + t = (nsigner_transport_t*)calloc(1, sizeof(*t)); + if (t == NULL) { + close(fd); + return NULL; + } + + ctx = (nsigner_fd_transport_ctx_t*)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + close(fd); + free(t); + return NULL; + } + + if (nsigner_fd_ctx_init(t, ctx, fd, fd, timeout_ms, 1, 1) != 0) { + close(fd); + free(ctx); + free(t); + return NULL; + } + + ctx->kind = NSIGNER_TRANSPORT_KIND_UNIX; + snprintf(ctx->socket_name, sizeof(ctx->socket_name), "%s", socket_name); + t->reconnect = nsigner_fd_reconnect; + + return t; +} + +static void nsigner_termios_make_raw(struct termios* tio) { + tio->c_iflag &= (tcflag_t)~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + tio->c_oflag &= (tcflag_t)~OPOST; + tio->c_lflag &= (tcflag_t)~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tio->c_cflag &= (tcflag_t)~(CSIZE | PARENB); + tio->c_cflag |= CS8; +} + +nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms) { + struct addrinfo hints; + struct addrinfo* addrs = NULL; + struct addrinfo* ai; + nsigner_transport_t* t = NULL; + nsigner_fd_transport_ctx_t* ctx = NULL; + char port_buf[16]; + int fd = -1; + + if (host == NULL || host[0] == '\0' || port <= 0 || port > 65535) { + return NULL; + } + + if (timeout_ms <= 0) { + timeout_ms = 5000; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + snprintf(port_buf, sizeof(port_buf), "%d", port); + if (getaddrinfo(host, port_buf, &hints, &addrs) != 0) { + return NULL; + } + + for (ai = addrs; ai != NULL; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) { + continue; + } + + if (nsigner_connect_with_timeout(fd, ai->ai_addr, ai->ai_addrlen, timeout_ms) == 0 && + nsigner_set_socket_timeouts(fd, timeout_ms) == 0) { + break; + } + + close(fd); + fd = -1; + } + + freeaddrinfo(addrs); + + if (fd < 0) { + return NULL; + } + + t = (nsigner_transport_t*)calloc(1, sizeof(*t)); + if (t == NULL) { + close(fd); + return NULL; + } + + ctx = (nsigner_fd_transport_ctx_t*)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + close(fd); + free(t); + return NULL; + } + + if (nsigner_fd_ctx_init(t, ctx, fd, fd, timeout_ms, 1, 1) != 0) { + close(fd); + free(ctx); + free(t); + return NULL; + } + + ctx->kind = NSIGNER_TRANSPORT_KIND_TCP; + snprintf(ctx->host, sizeof(ctx->host), "%s", host); + ctx->port = port; + t->reconnect = nsigner_fd_reconnect; + + return t; +} + +nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms) { + nsigner_transport_t* t = NULL; + nsigner_fd_transport_ctx_t* ctx = NULL; + struct termios tio; + int fd; + + if (device_path == NULL || device_path[0] == '\0') { + return NULL; + } + + fd = open(device_path, O_RDWR | O_NOCTTY | O_CLOEXEC); + if (fd < 0) { + return NULL; + } + + if (timeout_ms <= 0) { + timeout_ms = 5000; + } + + if (tcgetattr(fd, &tio) != 0) { + close(fd); + return NULL; + } + + nsigner_termios_make_raw(&tio); + tio.c_cflag |= (CLOCAL | CREAD); + tio.c_cflag &= ~(CRTSCTS | CSTOPB | PARENB); + tio.c_cflag = (tio.c_cflag & ~CSIZE) | CS8; + tio.c_iflag &= ~(IXON | IXOFF | IXANY); + tio.c_cc[VMIN] = 0; + tio.c_cc[VTIME] = 0; + + (void)cfsetispeed(&tio, B115200); + (void)cfsetospeed(&tio, B115200); + + if (tcsetattr(fd, TCSANOW, &tio) != 0) { + close(fd); + return NULL; + } + + t = (nsigner_transport_t*)calloc(1, sizeof(*t)); + if (t == NULL) { + close(fd); + return NULL; + } + + ctx = (nsigner_fd_transport_ctx_t*)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + close(fd); + free(t); + return NULL; + } + + if (nsigner_fd_ctx_init(t, ctx, fd, fd, timeout_ms, 1, 1) != 0) { + close(fd); + free(ctx); + free(t); + return NULL; + } + + ctx->kind = NSIGNER_TRANSPORT_KIND_SERIAL; + snprintf(ctx->device_path, sizeof(ctx->device_path), "%s", device_path); + t->reconnect = nsigner_fd_reconnect; + + return t; +} + +nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms) { + nsigner_transport_t* t = NULL; + nsigner_fd_transport_ctx_t* ctx = NULL; + + if (read_fd < 0 || write_fd < 0) { + return NULL; + } + + if (timeout_ms <= 0) { + timeout_ms = 5000; + } + + t = (nsigner_transport_t*)calloc(1, sizeof(*t)); + if (t == NULL) { + return NULL; + } + + ctx = (nsigner_fd_transport_ctx_t*)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + free(t); + return NULL; + } + + if (nsigner_fd_ctx_init(t, ctx, read_fd, write_fd, timeout_ms, 1, 1) != 0) { + free(ctx); + free(t); + return NULL; + } + + return t; +} + +int nsigner_transport_list_unix(char names[][64], int max_names) { + FILE* fp; + char line[512]; + int count = 0; + + if (max_names <= 0 || names == NULL) { + return 0; + } + + fp = fopen("/proc/net/unix", "r"); + if (fp == NULL) { + return 0; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char* at; + char* end; + size_t len; + int duplicate = 0; + int i; + + at = strrchr(line, '@'); + if (at == NULL) { + continue; + } + + end = at; + while (*end != '\0' && *end != '\n' && *end != ' ' && *end != '\t') { + end++; + } + + len = (size_t)(end - at); + if (len < 2) { + continue; + } + if (strncmp(at, "@nsigner", 8) != 0) { + continue; + } + + for (i = 0; i < count; i++) { + if (strncmp(names[i], at + 1, 63) == 0) { + duplicate = 1; + break; + } + } + if (duplicate) { + continue; + } + + if (count < max_names) { + size_t copy_len = len - 1; /* strip @ */ + if (copy_len > 63) { + copy_len = 63; + } + memcpy(names[count], at + 1, copy_len); + names[count][copy_len] = '\0'; + count++; + } else { + break; + } + } + + fclose(fp); + return count; +} + +int nsigner_transport_list_serial(char paths[][64], int max_paths) { + DIR* dir; + struct dirent* ent; + int count = 0; + + if (paths == NULL || max_paths <= 0) { + return 0; + } + + dir = opendir("/dev"); + if (dir == NULL) { + return 0; + } + + while ((ent = readdir(dir)) != NULL) { + int n; + + if (strncmp(ent->d_name, "ttyACM", 6) != 0) { + continue; + } + + if (count >= max_paths) { + break; + } + + n = snprintf(paths[count], 64, "/dev/%s", ent->d_name); + if (n <= 0 || n >= 64) { + continue; + } + + count++; + } + + closedir(dir); + return count; +} + +#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */ diff --git a/nostr_core/nsigner_transport.h b/nostr_core/nsigner_transport.h new file mode 100644 index 00000000..5c621498 --- /dev/null +++ b/nostr_core/nsigner_transport.h @@ -0,0 +1,61 @@ +#ifndef NSIGNER_TRANSPORT_H +#define NSIGNER_TRANSPORT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(NOSTR_ENABLE_NSIGNER_CLIENT) + +typedef struct nsigner_transport nsigner_transport_t; + +struct nsigner_transport { + int (*send_framed)(nsigner_transport_t* t, const char* json, size_t len); + int (*recv_framed)(nsigner_transport_t* t, char** out_json, size_t* out_len); /* caller frees *out_json */ + /* + * Re-establish the underlying connection. The n_signer server closes the + * connection after handling a single framed request, so the client must + * reconnect before each call. Returns 0 on success, non-zero on failure. + * May be NULL for transports that do not support reconnection (e.g. fd-pair); + * callers should treat a NULL reconnect as "not supported". + */ + int (*reconnect)(nsigner_transport_t* t); + void (*close)(nsigner_transport_t* t); + void* ctx; +}; + +/* Raw framed I/O helpers for existing connected file descriptors. */ +int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len); +int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len); + +/* UNIX abstract socket transport (socket_name without leading '@'). */ +nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms); + +/* TCP transport (host + port), with IPv4/IPv6 resolution via getaddrinfo. */ +nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms); + +/* USB CDC-ACM serial transport (device path like /dev/ttyACM0). */ +nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms); + +/* + * Framed transport over existing file descriptors (read_fd, write_fd). + * close() closes both owned fds. If passing STDIN_FILENO/STDOUT_FILENO and you do not + * want them closed, pass dup()'d descriptors instead. + */ +nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms); + +/* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */ +int nsigner_transport_list_unix(char names[][64], int max_names); + +/* Best-effort enumerate serial devices (e.g. /dev/ttyACM*), returns count copied. */ +int nsigner_transport_list_serial(char paths[][64], int max_paths); + +#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */ + +#ifdef __cplusplus +} +#endif + +#endif /* NSIGNER_TRANSPORT_H */ diff --git a/nostr_core/utils.c b/nostr_core/utils.c index 06545930..46b80ef2 100644 --- a/nostr_core/utils.c +++ b/nostr_core/utils.c @@ -433,32 +433,38 @@ int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash) { } int nostr_sha256_file_stream(const char* filename, unsigned char* hash) { +#ifndef NOSTR_NO_FILESYSTEM if (!filename || !hash) return -1; - + FILE* file = fopen(filename, "rb"); if (!file) return -1; - + nostr_sha256_ctx_t ctx; if (nostr_sha256_init(&ctx) != 0) { fclose(file); return -1; } - + // Process file in 4KB chunks for memory efficiency unsigned char buffer[4096]; size_t bytes_read; - + while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) { if (nostr_sha256_update(&ctx, buffer, bytes_read) != 0) { fclose(file); return -1; } } - + fclose(file); - + // Finalize and return result return nostr_sha256_final(&ctx, hash); +#else + (void)filename; + (void)hash; + return -1; +#endif } // ============================================================================= diff --git a/nostr_websocket/nostr_websocket_openssl.c b/nostr_websocket/nostr_websocket_openssl.c index fad86137..bf365e56 100644 --- a/nostr_websocket/nostr_websocket_openssl.c +++ b/nostr_websocket/nostr_websocket_openssl.c @@ -12,6 +12,18 @@ #include #include #include +#include + +static int ws_connect_debug_enabled(void) { + static int initialized = 0; + static int enabled = 0; + if (!initialized) { + const char* env = getenv("NOSTR_WS_CONNECT_DEBUG"); + enabled = (env && env[0] != '\0' && strcmp(env, "0") != 0); + initialized = 1; + } + return enabled; +} // OpenSSL headers #include @@ -23,7 +35,7 @@ #define WS_MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" #define WS_KEY_LEN 24 #define MAX_HEADER_SIZE 4096 -#define MAX_FRAME_SIZE 65536 +#define MAX_FRAME_SIZE 262144 // Transport layer abstraction typedef struct { @@ -418,32 +430,142 @@ const char* nostr_ws_strerror(int error_code) { static int tcp_connect(void* ctx, const char* host, int port) { tcp_transport_t* tcp = (tcp_transport_t*)ctx; - - // Create socket - tcp->socket_fd = socket(AF_INET, SOCK_STREAM, 0); + struct addrinfo hints; + struct addrinfo* result = NULL; + struct addrinfo* rp = NULL; + char port_str[16]; + int ret; + int addr_idx = 0; + time_t connect_start = time(NULL); + + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] tcp_connect_start host=%s port=%d\n", host ? host : "(null)", port); + } + + tcp->socket_fd = -1; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; // IPv4 or IPv6 + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + snprintf(port_str, sizeof(port_str), "%d", port); + ret = getaddrinfo(host, port_str, &hints, &result); + if (ret != 0 || !result) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] getaddrinfo_failed host=%s port=%d ret=%d err=%s\n", + host ? host : "(null)", + port, + ret, + gai_strerror(ret)); + } + return -1; + } + + for (rp = result; rp != NULL; rp = rp->ai_next) { + int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + addr_idx++; + if (fd < 0) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] socket_failed host=%s port=%d addr_idx=%d family=%d errno=%d\n", + host ? host : "(null)", + port, + addr_idx, + rp->ai_family, + errno); + } + continue; + } + + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] connect_attempt host=%s port=%d addr_idx=%d family=%d\n", + host ? host : "(null)", + port, + addr_idx, + rp->ai_family); + } + + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) { + (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } + + int connect_rc = connect(fd, rp->ai_addr, rp->ai_addrlen); + if (connect_rc == 0) { + tcp->socket_fd = fd; + if (flags >= 0) { + (void)fcntl(fd, F_SETFL, flags); + } + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] connect_success host=%s port=%d addr_idx=%d elapsed_s=%ld\n", + host ? host : "(null)", + port, + addr_idx, + (long)(time(NULL) - connect_start)); + } + break; + } + + if (connect_rc < 0 && errno == EINPROGRESS) { + fd_set wfds; + struct timeval tv; + int sel_rc; + int so_error = 0; + socklen_t so_error_len = sizeof(so_error); + + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + tv.tv_sec = 5; + tv.tv_usec = 0; + + sel_rc = select(fd + 1, NULL, &wfds, NULL, &tv); + if (sel_rc > 0 && FD_ISSET(fd, &wfds) && + getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_error_len) == 0 && + so_error == 0) { + tcp->socket_fd = fd; + if (flags >= 0) { + (void)fcntl(fd, F_SETFL, flags); + } + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] connect_success_after_wait host=%s port=%d addr_idx=%d elapsed_s=%ld\n", + host ? host : "(null)", + port, + addr_idx, + (long)(time(NULL) - connect_start)); + } + break; + } + + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] connect_timeout_or_error host=%s port=%d addr_idx=%d select_rc=%d so_error=%d elapsed_s=%ld\n", + host ? host : "(null)", + port, + addr_idx, + sel_rc, + so_error, + (long)(time(NULL) - connect_start)); + } + } else if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] connect_failed host=%s port=%d addr_idx=%d errno=%d elapsed_s=%ld\n", + host ? host : "(null)", + port, + addr_idx, + errno, + (long)(time(NULL) - connect_start)); + } + + close(fd); + } + + freeaddrinfo(result); + if (tcp->socket_fd < 0) { - return -1; - } - - // Resolve hostname - struct hostent* he = gethostbyname(host); - if (!he) { - close(tcp->socket_fd); - tcp->socket_fd = -1; - return -1; - } - - // Set up address - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length); - - // Connect - if (connect(tcp->socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - close(tcp->socket_fd); - tcp->socket_fd = -1; + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] tcp_connect_end host=%s port=%d rc=-1 elapsed_s=%ld\n", + host ? host : "(null)", + port, + (long)(time(NULL) - connect_start)); + } return -1; } @@ -453,7 +575,14 @@ static int tcp_connect(void* ctx, const char* host, int port) { io_timeout.tv_usec = 0; (void)setsockopt(tcp->socket_fd, SOL_SOCKET, SO_RCVTIMEO, &io_timeout, sizeof(io_timeout)); (void)setsockopt(tcp->socket_fd, SOL_SOCKET, SO_SNDTIMEO, &io_timeout, sizeof(io_timeout)); - + + if (ws_connect_debug_enabled()) { + fprintf(stderr, "[nostr_ws][connect] tcp_connect_end host=%s port=%d rc=0 elapsed_s=%ld\n", + host ? host : "(null)", + port, + (long)(time(NULL) - connect_start)); + } + return 0; } @@ -881,13 +1010,22 @@ static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const ch static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char* payload, size_t* payload_len, int timeout_ms) { if (!client || !opcode || !payload || !payload_len) return -1; - char header[14]; // Max header size + unsigned char header[14]; // Max header size size_t header_len = 2; // Minimum header size // Read basic header int header_result = client->transport->recv(&client->transport_ctx, header, 2, timeout_ms); if (header_result != 2) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] short_header host=%s port=%d got=%d expected=2 timeout_ms=%d errno=%d\n", + client->host ? client->host : "(null)", + client->port, + header_result, + timeout_ms, + errno); + } return -1; } @@ -899,12 +1037,28 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char // Extended length if (len == 126) { if (client->transport->recv(&client->transport_ctx, header + 2, 2, timeout_ms) != 2) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] short_ext16 host=%s port=%d timeout_ms=%d errno=%d\n", + client->host ? client->host : "(null)", + client->port, + timeout_ms, + errno); + } return -1; } len = ((uint16_t)header[2] << 8) | (uint8_t)header[3]; header_len = 4; } else if (len == 127) { if (client->transport->recv(&client->transport_ctx, header + 2, 8, timeout_ms) != 8) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] short_ext64 host=%s port=%d timeout_ms=%d errno=%d\n", + client->host ? client->host : "(null)", + client->port, + timeout_ms, + errno); + } return -1; } // For simplicity, we don't support 64-bit lengths @@ -917,6 +1071,14 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char uint32_t mask = 0; if (masked) { if (client->transport->recv(&client->transport_ctx, header + header_len, 4, timeout_ms) != 4) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] short_mask host=%s port=%d timeout_ms=%d errno=%d\n", + client->host ? client->host : "(null)", + client->port, + timeout_ms, + errno); + } return -1; } mask = ((uint32_t)header[header_len] << 24) | @@ -930,10 +1092,30 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char if (len > *payload_len) { char discard[1024]; uint64_t remaining = len; + int drain_timeout_ms = timeout_ms < 5000 ? 5000 : timeout_ms; + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] payload_too_large host=%s port=%d frame_len=%llu buffer_cap=%zu drain_timeout_ms=%d\n", + client->host ? client->host : "(null)", + client->port, + (unsigned long long)len, + *payload_len, + drain_timeout_ms); + } while (remaining > 0) { size_t chunk = remaining > sizeof(discard) ? sizeof(discard) : (size_t)remaining; - int got = client->transport->recv(&client->transport_ctx, discard, chunk, timeout_ms); + int got = client->transport->recv(&client->transport_ctx, discard, chunk, drain_timeout_ms); if (got <= 0) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] discard_read_failed host=%s port=%d got=%d drain_timeout_ms=%d errno=%d remaining=%llu\n", + client->host ? client->host : "(null)", + client->port, + got, + drain_timeout_ms, + errno, + (unsigned long long)remaining); + } return -1; } remaining -= (uint64_t)got; @@ -949,7 +1131,19 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char payload + received, len - received, timeout_ms); - if (chunk <= 0) return -1; + if (chunk <= 0) { + if (ws_connect_debug_enabled()) { + fprintf(stderr, + "[nostr_ws][frame] payload_read_failed host=%s port=%d got=%d need_remaining=%zu timeout_ms=%d errno=%d\n", + client->host ? client->host : "(null)", + client->port, + chunk, + len - received, + timeout_ms, + errno); + } + return -1; + } received += chunk; } diff --git a/plans/nsigner_integration_plan.md b/plans/nsigner_integration_plan.md new file mode 100644 index 00000000..67a841fc --- /dev/null +++ b/plans/nsigner_integration_plan.md @@ -0,0 +1,208 @@ +# n_signer Integration into nostr_core_lib β€” Architecture Plan + +> Status: M1–M6 implemented. This plan remains as architecture history; for current usage/integration contract see: +> - `README.md` β†’ "Signer abstraction & nsigner integration" +> - `nostr_core/NSIGNER_INTEGRATION.md` +> +> This plan originally described pulling the **caller-side** signer-integration glue out of per-project implementations and into `nostr_core_lib`, +> so any project that already links the library can sign **locally**, via a **running +> n_signer process**, or via a **USB hardware signer** β€” with the same code. + +## 1. Problem + +Many projects (`nostr_terminal`, `nostr_push`, `n_os_tr`, `nostr_login_lite`) each +re-implement the *same* caller-side integration with n_signer: + +- 4-byte big-endian length-prefixed framing +- kind-27235 auth envelope (`nsigner_rpc` / `nsigner_method` / `nsigner_body_hash`) +- the JSON-RPC verbs (`get_public_key`, `sign_event`, `nip04_*`, `nip44_*`) +- a per-project `signer.h` abstraction wrapping local-vs-remote signing + +Meanwhile, `nostr_core_lib` exposes ~30+ functions that take a raw +`const unsigned char* private_key` and sign/encrypt in-process (e.g. +[`nostr_create_and_sign_event()`](../nostr_core/nip001.h:16)), with no seam to +substitute a remote signer. + +The dependency direction is already **n_signer β†’ nostr_core_lib** (n_signer vendors +nostr_core_lib and uses its crypto). So the natural home for the shared, **key-free**, +caller-side glue is `nostr_core_lib` itself. This is not recursive: nothing +key-bearing and nothing from the signer *program* moves down. + +## 2. What moves vs. what stays + +**Moves into `nostr_core_lib`** (caller-side, key-free, shared): +- `nostr_signer_t` abstraction (the backend-agnostic seam). +- Local backend (wraps today's raw-privkey path; zero behavior change). +- Shared nsigner client: framing + kind-27235 auth envelope + JSON-RPC verbs + discovery. +- Caller-side transports: AF_UNIX abstract socket, TCP, stdio, **USB-CDC serial**. +- Parallel `*_with_signer` entry points. + +**Stays in `n_signer`** (the program + host/device specifics): +- The signer **binary** itself: mnemonic, role derivation, purpose/curve enforcement, + attended TUI, approval prompts β€” the **trust boundary**. Must not move. +- Spawn/embed helper (memfd + fexecve + terminal launch) β€” desktop-Linux deployment. +- **Hardware-signer firmware** (ESP32/Feather, TinyUSB) β€” developing the device. +- Browser WebUSB/WebSerial drivers (JS) β€” stay in `nostr_login_lite`. +- Qubes packaging, FIPS, Docker. + +```mermaid +graph TD + subgraph CORE[nostr_core_lib] + ABS[nostr_signer_t abstraction] + LOCAL[Local backend] + CLIENT[nsigner client: framing + kind-27235 auth + discovery] + WS[signer-aware *_with_signer entry points] + TRANS[Transports: unix / tcp / stdio / usb-cdc serial] + end + subgraph NS[n_signer] + BIN[Signer binary: mnemonic, roles, TUI, approvals] + SPAWN[Spawn/embed helper] + FW[Hardware firmware ESP32/Feather] + end + PROJ[Downstream project] --> CORE + WS -.wire.-> BIN + TRANS -.usb-cdc.-> FW + SPAWN -.launches.-> BIN + NS --> CORE +``` + +## 3. Design decisions (locked) + +- **Parallel APIs (Option A).** Add `*_with_signer` variants; leave all existing + raw-privkey APIs byte-for-byte untouched. Existing projects keep compiling and + behaving identically; remote signing is opt-in per call site. +- **Scope = Option 2.** Abstraction + local backend + shared nsigner client (incl. + transports) live in `nostr_core_lib`. The spawn/embed helper and the device firmware + remain in n_signer. +- **USB hardware signer is a caller-side transport.** Desktop C apps reach a USB-CDC + hardware signer over `/dev/ttyACM*` using the *same* framing/auth/verbs as the socket + transports. The browser WebUSB path and the firmware are out of scope here. +- **Build-flag gated.** The remote client + desktop transports compile only when + `NOSTR_ENABLE_NSIGNER_CLIENT` is set β€” ON for desktop, OFF for ESP32 builds and OFF + for n_signer's own build. The abstraction + local backend compile everywhere. + +## 4. Module layout (new files) + +- `nostr_core/nostr_signer.{h,c}` β€” `nostr_signer_t` + vtable + local backend + the + six verb entry points operating on a handle. +- `nostr_core/nsigner_client.{h,c}` β€” framing, JSON-RPC verbs, kind-27235 auth envelope + builder, error mapping, discovery; remote backend that adapts a connection into a + `nostr_signer_t`. (Guarded by `NOSTR_ENABLE_NSIGNER_CLIENT`.) +- `nostr_core/nsigner_transport.{h,c}` β€” pluggable transport vtable + (`open / send_framed / recv_framed / close`) with unix-abstract, tcp, stdio, and + usb-cdc serial implementations. + +## 5. Signer abstraction (sketch) + +```c +typedef struct nostr_signer nostr_signer_t; + +/* Verbs mirror n_signer's wire contract */ +int nostr_signer_get_public_key(nostr_signer_t*, char out_pubkey_hex[65]); +int nostr_signer_sign_event(nostr_signer_t*, cJSON* unsigned_event, cJSON** signed_out); +int nostr_signer_nip04_encrypt(nostr_signer_t*, const char* peer_hex, const char* pt, char** ct); +int nostr_signer_nip04_decrypt(nostr_signer_t*, const char* peer_hex, const char* ct, char** pt); +int nostr_signer_nip44_encrypt(nostr_signer_t*, const char* peer_hex, const char* pt, char** ct); +int nostr_signer_nip44_decrypt(nostr_signer_t*, const char* peer_hex, const char* ct, char** pt); +void nostr_signer_free(nostr_signer_t*); + +/* Local backend (always available) */ +nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]); + +/* Remote backends (only with NOSTR_ENABLE_NSIGNER_CLIENT) */ +nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, + const unsigned char caller_key[32]); +nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, + const unsigned char caller_key[32]); +nostr_signer_t* nostr_signer_nsigner_serial(const char* dev_path, const char* role, + const unsigned char caller_key[32]); +``` + +The local backend simply calls the existing in-process crypto; the remote backends +serialize each verb into a framed JSON-RPC request, attach the kind-27235 auth envelope, +and parse the response. + +## 6. Signer-aware entry points (Phase 3 targets) + +Add parallel functions, highest value first: +1. `nostr_create_and_sign_event_with_signer(...)` β€” the workhorse. +2. NIP-17 DM creation (needs nip44 encrypt + sign). +3. NIP-46 client session signing. +4. NIP-60 / NIP-61 wallet/token/nutzap events. +5. blossom auth header creation. +6. relay-pool NIP-42 auth (`nostr_relay_pool_set_auth` signer-aware variant). + +Every one keeps its existing raw-privkey sibling unchanged. + +## 7. Phasing + +- **Phase 0** β€” this doc. +- **Phase 1** β€” `nostr_signer_t` + local backend (portable, ESP32-safe). +- **Phase 2** β€” transport vtable; shared nsigner client (framing/auth/verbs/discovery); + unix/tcp/stdio/usb-cdc transports; remote backend. +- **Phase 3** β€” `*_with_signer` entry points (order in Β§6). +- **Phase 4** β€” build-flag gating; verify n_signer + ESP32 exclude remote pieces; + confirm dependency stays n_signer β†’ nostr_core_lib only. +- **Phase 5** β€” example: identical code signing local vs running nsigner (unix) vs USB + hardware signer; tests against a live nsigner instance. +- **Phase 6** β€” README/integration docs; cross-link the per-project plans this supersedes. +- **Phase 7** (optional) β€” migrate n_signer's `client/` and the per-project hand-rolled + clients onto the shared module; design spawn helper as a clean add-on for later + promotion into the core if desired. + +## 8. Risks / open items + +- **Auth envelope canonicalization (JCS).** Must match n_signer exactly + (`SHA-256(JCS(params))`, `SHA-256("null")` for no-params). Verify against + [`caller_token_identity.md`](../../n_signer/plans/caller_token_identity.md:96). +- **Serial transport identity.** USB-CDC is an "asserted caller"; approval prompts on the + device still apply. Client must treat `approval_denied` as a normal outcome. +- **Memory ownership.** Document free() responsibilities for returned JSON/strings, and + zeroization of any caller key buffers. +- **Versioning of the wire contract.** Centralizing it here makes nostr_core_lib the + single source of truth; n_signer should track it rather than diverge. + +## 9. Driver / acceptance app: `examples/note_poster` + +The work is driven by the simplest possible real Nostr app: **sign in, then post one +kind-1 note**. It is the acceptance test for the abstraction β€” the app is written once +and only the signer construction at sign-in changes. + +### Behavior +1. Sign in β†’ `nostr_signer_get_public_key()` resolves the npub. +2. Prompt for note text β†’ `nostr_create_and_sign_event_with_signer(kind=1, ...)`. +3. Publish the signed event to **`wss://relay.laantungir.net`** using the existing relay + pool (see [`examples/relay_pool.c`](../examples/relay_pool.c)). +4. Print the event id and the relay's OK response. + +### The whole point (one line differs per backend) +```c +nostr_signer_t *signer; +switch (mode) { + case LOCAL: signer = nostr_signer_local(privkey); break; + case UNIX: signer = nostr_signer_nsigner_unix(socket_name, "main", caller_key); break; + case SERIAL: signer = nostr_signer_nsigner_serial("/dev/ttyACM0", "main", caller_key);break; +} +/* everything below is identical regardless of backend */ +``` + +### Milestones (each independently demoable with the same app) +- **M1 β€” local key, real publish.** `nostr_signer_t` + local backend + + `nostr_create_and_sign_event_with_signer`; app posts a kind-1 to + `wss://relay.laantungir.net` using a local nsec. No remote/transport/auth code yet. +- **M2 β€” running nsigner over unix socket.** Add framing + kind-27235 auth + unix + transport + remote backend. Same app, `--signer unix:`, posts the note via a + running `nsigner`. +- **M3 β€” USB hardware signer.** Add the USB-CDC serial transport. Same app, + `--signer serial:/dev/ttyACM0`, posts the note via the hardware signer. + +### CLI shape +``` +note_poster --signer local --nsec +note_poster --signer unix: --caller-nsec [--role main] +note_poster --signer serial:/dev/ttyACM0 --caller-nsec [--role main] +``` + +### Acceptance criteria +A kind-1 note authored by the same code reaches `wss://relay.laantungir.net` and returns +a relay OK, across all three signer backends, with only the sign-in line differing. diff --git a/platform/esp32/nostr_http_esp32.c b/platform/esp32/nostr_http_esp32.c new file mode 100644 index 00000000..5ebbd066 --- /dev/null +++ b/platform/esp32/nostr_http_esp32.c @@ -0,0 +1,312 @@ +#include "../../nostr_core/nostr_http.h" + +#include "esp_http_client.h" + +#include +#include +#include + +typedef struct { + char* data; + size_t len; + size_t cap; + size_t max_bytes; + int truncated; +} nostr_http_buffer_t; + +static char g_ca_bundle_path[512] = {0}; + +static char* nostr_http_strdup_local(const char* s) { + if (!s) return NULL; + size_t n = strlen(s); + char* out = (char*)malloc(n + 1U); + if (!out) return NULL; + memcpy(out, s, n + 1U); + return out; +} + +static int append_bytes(nostr_http_buffer_t* b, const void* src, size_t n) { + if (!b || !src || n == 0) return 1; + + if (b->max_bytes > 0 && b->len >= b->max_bytes) { + b->truncated = 1; + return 1; + } + + size_t to_copy = n; + if (b->max_bytes > 0) { + size_t remaining = b->max_bytes - b->len; + if (to_copy > remaining) { + to_copy = remaining; + b->truncated = 1; + } + } + + if (b->len + to_copy + 1U > b->cap) { + size_t new_cap = b->cap == 0 ? 1024U : b->cap; + while (new_cap < b->len + to_copy + 1U) { + new_cap *= 2U; + } + char* p = (char*)realloc(b->data, new_cap); + if (!p) return 0; + b->data = p; + b->cap = new_cap; + } + + memcpy(b->data + b->len, src, to_copy); + b->len += to_copy; + b->data[b->len] = '\0'; + return 1; +} + +typedef struct { + nostr_http_buffer_t* body; + nostr_http_buffer_t* headers; + int capture_headers; +} nostr_http_event_ctx_t; + +static esp_err_t http_event_handler(esp_http_client_event_t* evt) { + if (!evt || !evt->user_data) return ESP_OK; + + nostr_http_event_ctx_t* ctx = (nostr_http_event_ctx_t*)evt->user_data; + + switch (evt->event_id) { + case HTTP_EVENT_ON_DATA: + if (evt->data && evt->data_len > 0 && ctx->body) { + if (!append_bytes(ctx->body, evt->data, (size_t)evt->data_len)) { + return ESP_FAIL; + } + } + break; + + case HTTP_EVENT_ON_HEADER: + if (ctx->capture_headers && ctx->headers && evt->header_key && evt->header_value) { + if (!append_bytes(ctx->headers, evt->header_key, strlen(evt->header_key)) || + !append_bytes(ctx->headers, ": ", 2) || + !append_bytes(ctx->headers, evt->header_value, strlen(evt->header_value)) || + !append_bytes(ctx->headers, "\r\n", 2)) { + return ESP_FAIL; + } + } + break; + + default: + break; + } + + return ESP_OK; +} + +void nostr_http_set_ca_bundle(const char* ca_bundle_path) { + if (!ca_bundle_path || ca_bundle_path[0] == '\0') { + g_ca_bundle_path[0] = '\0'; + return; + } + + strncpy(g_ca_bundle_path, ca_bundle_path, sizeof(g_ca_bundle_path) - 1); + g_ca_bundle_path[sizeof(g_ca_bundle_path) - 1] = '\0'; +} + +const char* nostr_http_detect_ca_bundle(void) { + if (g_ca_bundle_path[0] != '\0') { + return g_ca_bundle_path; + } + return NULL; +} + +int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp) { + if (!req || !req->url || !resp) return NOSTR_ERROR_INVALID_INPUT; + + memset(resp, 0, sizeof(*resp)); + + const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET"; + int timeout_ms = (req->timeout_seconds > 0 ? req->timeout_seconds : 30) * 1000; + + nostr_http_buffer_t body = {0}; + body.max_bytes = req->max_response_bytes; + + nostr_http_buffer_t headers = {0}; + + nostr_http_event_ctx_t evt_ctx = { + .body = &body, + .headers = &headers, + .capture_headers = req->capture_headers + }; + + esp_http_client_config_t cfg = { + .url = req->url, + .timeout_ms = timeout_ms, + .event_handler = http_event_handler, + .user_data = &evt_ctx, + .disable_auto_redirect = req->follow_redirects ? false : true, + .max_redirection_count = req->max_redirects > 0 ? req->max_redirects : 3, + }; + + esp_http_client_handle_t client = esp_http_client_init(&cfg); + if (!client) { + return NOSTR_ERROR_NETWORK_FAILED; + } + + if (req->user_agent && req->user_agent[0] != '\0') { + esp_http_client_set_header(client, "User-Agent", req->user_agent); + } else { + esp_http_client_set_header(client, "User-Agent", "nostr-core/1.0"); + } + + if (req->headers) { + for (const char** h = req->headers; *h; h++) { + const char* line = *h; + if (!line || line[0] == '\0') continue; + const char* colon = strchr(line, ':'); + if (!colon) continue; + + size_t key_len = (size_t)(colon - line); + if (key_len == 0) continue; + + char* key = (char*)malloc(key_len + 1U); + if (!key) { + esp_http_client_cleanup(client); + free(body.data); + free(headers.data); + return NOSTR_ERROR_MEMORY_FAILED; + } + + memcpy(key, line, key_len); + key[key_len] = '\0'; + + const char* value = colon + 1; + while (*value == ' ') value++; + + esp_http_client_set_header(client, key, value); + free(key); + } + } + + if (strcasecmp(method, "GET") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_GET); + } else if (strcasecmp(method, "POST") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_POST); + if (req->body && req->body_len > 0) { + esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len); + } else { + esp_http_client_set_post_field(client, "", 0); + } + } else if (strcasecmp(method, "HEAD") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_HEAD); + } else if (strcasecmp(method, "PUT") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_PUT); + if (req->body && req->body_len > 0) { + esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len); + } + } else if (strcasecmp(method, "DELETE") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_DELETE); + } else { + esp_http_client_cleanup(client); + return NOSTR_ERROR_INVALID_INPUT; + } + + esp_err_t err = esp_http_client_perform(client); + if (err != ESP_OK) { + esp_http_client_cleanup(client); + free(body.data); + free(headers.data); + return NOSTR_ERROR_NETWORK_FAILED; + } + + resp->status_code = esp_http_client_get_status_code(client); + resp->body = body.data ? body.data : nostr_http_strdup_local(""); + resp->body_len = body.data ? body.len : 0; + resp->headers_raw = headers.data; + resp->truncated = body.truncated; + + char* ct = NULL; + char* ct_raw = NULL; + if (esp_http_client_get_header(client, "Content-Type", &ct_raw) == ESP_OK && ct_raw && ct_raw[0] != '\0') { + ct = nostr_http_strdup_local(ct_raw); + } + resp->content_type = ct; + + esp_http_client_cleanup(client); + + if (!resp->body) { + free(resp->headers_raw); + free(resp->content_type); + memset(resp, 0, sizeof(*resp)); + return NOSTR_ERROR_MEMORY_FAILED; + } + + return NOSTR_SUCCESS; +} + +void nostr_http_response_free(nostr_http_response_t* resp) { + if (!resp) return; + free(resp->body); + free(resp->content_type); + free(resp->headers_raw); + memset(resp, 0, sizeof(*resp)); +} + +int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out) { + if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT; + + *body_out = NULL; + if (status_out) *status_out = 0; + + nostr_http_request_t req; + memset(&req, 0, sizeof(req)); + req.method = "GET"; + req.url = url; + req.timeout_seconds = timeout_seconds; + req.follow_redirects = 1; + req.max_redirects = 3; + + nostr_http_response_t resp; + int rc = nostr_http_request(&req, &resp); + if (rc != NOSTR_SUCCESS) return rc; + + if (status_out) *status_out = resp.status_code; + *body_out = resp.body; + free(resp.content_type); + free(resp.headers_raw); + return NOSTR_SUCCESS; +} + +int nostr_http_post_json(const char* url, + const char* json_body, + int timeout_seconds, + char** body_out, + long* status_out) { + if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT; + + *body_out = NULL; + if (status_out) *status_out = 0; + + const char* headers[] = { + "Accept: application/json", + "Content-Type: application/json", + NULL + }; + + const char* payload = json_body ? json_body : "{}"; + + nostr_http_request_t req; + memset(&req, 0, sizeof(req)); + req.method = "POST"; + req.url = url; + req.headers = headers; + req.body = (const unsigned char*)payload; + req.body_len = strlen(payload); + req.timeout_seconds = timeout_seconds; + req.follow_redirects = 1; + req.max_redirects = 3; + + nostr_http_response_t resp; + int rc = nostr_http_request(&req, &resp); + if (rc != NOSTR_SUCCESS) return rc; + + if (status_out) *status_out = resp.status_code; + *body_out = resp.body; + free(resp.content_type); + free(resp.headers_raw); + return NOSTR_SUCCESS; +} diff --git a/platform/esp32/nostr_platform_esp32.c b/platform/esp32/nostr_platform_esp32.c new file mode 100644 index 00000000..1192ce1b --- /dev/null +++ b/platform/esp32/nostr_platform_esp32.c @@ -0,0 +1,12 @@ +#include "../../nostr_core/nostr_platform.h" + +#include "esp_random.h" + +int nostr_platform_random(unsigned char *buf, size_t len) { + if (buf == NULL || len == 0) { + return -1; + } + + esp_fill_random(buf, len); + return 0; +} diff --git a/platform/esp32/nostr_websocket_esp32.c b/platform/esp32/nostr_websocket_esp32.c new file mode 100644 index 00000000..35599915 --- /dev/null +++ b/platform/esp32/nostr_websocket_esp32.c @@ -0,0 +1,457 @@ +#include "../../nostr_websocket/nostr_websocket_tls.h" + +#include "esp_crt_bundle.h" +#include "esp_err.h" +#include "esp_log.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +typedef struct { + char* host; + int port; + char* path; + int use_tls; +} ws_url_parts_t; + +struct nostr_ws_client { + esp_transport_list_handle_t list; + esp_transport_handle_t transport; + nostr_ws_state_t state; + int timeout_ms; + char* host; + int port; + char* path; +}; + +static void ws_free_url_parts(ws_url_parts_t* p) { + if (!p) return; + free(p->host); + free(p->path); + p->host = NULL; + p->path = NULL; + p->port = 0; + p->use_tls = 0; +} + +static int ws_parse_url(const char* url, ws_url_parts_t* out) { + if (!url || !out) return -1; + + memset(out, 0, sizeof(*out)); + + const char* p = NULL; + if (strncmp(url, "ws://", 5) == 0) { + out->use_tls = 0; + out->port = 80; + p = url + 5; + } else if (strncmp(url, "wss://", 6) == 0) { + out->use_tls = 1; + out->port = 443; + p = url + 6; + } else { + return -1; + } + + const char* slash = strchr(p, '/'); + const char* host_end = slash ? slash : (p + strlen(p)); + + const char* colon = NULL; + for (const char* it = p; it < host_end; ++it) { + if (*it == ':') { + colon = it; + break; + } + } + + size_t host_len; + if (colon) { + host_len = (size_t)(colon - p); + char portbuf[8] = {0}; + size_t port_len = (size_t)(host_end - colon - 1); + if (port_len == 0 || port_len >= sizeof(portbuf)) return -1; + memcpy(portbuf, colon + 1, port_len); + for (size_t i = 0; i < port_len; i++) { + if (!isdigit((unsigned char)portbuf[i])) return -1; + } + out->port = atoi(portbuf); + if (out->port <= 0 || out->port > 65535) return -1; + } else { + host_len = (size_t)(host_end - p); + } + + if (host_len == 0) return -1; + + out->host = (char*)malloc(host_len + 1U); + if (!out->host) return -1; + memcpy(out->host, p, host_len); + out->host[host_len] = '\0'; + + out->path = slash ? strdup(slash) : strdup("/"); + if (!out->path) { + ws_free_url_parts(out); + return -1; + } + + return 0; +} + +static void ws_client_destroy(struct nostr_ws_client* c) { + if (!c) return; + + if (c->transport) { + esp_transport_close(c->transport); + c->transport = NULL; + } + + if (c->list) { + esp_transport_list_destroy(c->list); + c->list = NULL; + } + + free(c->host); + free(c->path); + free(c); +} + +static esp_transport_handle_t ws_build_transport(const ws_url_parts_t* up, + esp_transport_list_handle_t list) { + if (!up || !list) return NULL; + + if (up->use_tls) { + esp_transport_handle_t ssl = esp_transport_ssl_init(); + if (!ssl) return NULL; + + esp_transport_ssl_crt_bundle_attach(ssl, esp_crt_bundle_attach); + esp_transport_set_default_port(ssl, 443); + if (esp_transport_list_add(list, ssl, "ssl") != ESP_OK) { + esp_transport_destroy(ssl); + return NULL; + } + + esp_transport_handle_t wss = esp_transport_ws_init(ssl); + if (!wss) return NULL; + + esp_transport_set_default_port(wss, up->port); + esp_transport_ws_set_path(wss, up->path); + if (esp_transport_ws_set_user_agent(wss, "nostr_core_lib/esp32") != ESP_OK) { + esp_transport_destroy(wss); + return NULL; + } + if (esp_transport_list_add(list, wss, "wss") != ESP_OK) { + esp_transport_destroy(wss); + return NULL; + } + return wss; + } + + esp_transport_handle_t tcp = esp_transport_tcp_init(); + if (!tcp) return NULL; + + esp_transport_set_default_port(tcp, 80); + if (esp_transport_list_add(list, tcp, "tcp") != ESP_OK) { + esp_transport_destroy(tcp); + return NULL; + } + + esp_transport_handle_t ws = esp_transport_ws_init(tcp); + if (!ws) return NULL; + + esp_transport_set_default_port(ws, up->port); + esp_transport_ws_set_path(ws, up->path); + if (esp_transport_ws_set_user_agent(ws, "nostr_core_lib/esp32") != ESP_OK) { + esp_transport_destroy(ws); + return NULL; + } + if (esp_transport_list_add(list, ws, "ws") != ESP_OK) { + esp_transport_destroy(ws); + return NULL; + } + + return ws; +} + +nostr_ws_client_t* nostr_ws_connect(const char* url) { + if (!url) return NULL; + + ws_url_parts_t up; + if (ws_parse_url(url, &up) != 0) return NULL; + + struct nostr_ws_client* c = (struct nostr_ws_client*)calloc(1, sizeof(struct nostr_ws_client)); + if (!c) { + ws_free_url_parts(&up); + return NULL; + } + + c->list = esp_transport_list_init(); + if (!c->list) { + ws_free_url_parts(&up); + free(c); + return NULL; + } + + c->transport = ws_build_transport(&up, c->list); + if (!c->transport) { + ws_free_url_parts(&up); + ws_client_destroy(c); + return NULL; + } + + c->timeout_ms = 30000; + c->state = NOSTR_WS_CONNECTING; + c->host = up.host; + c->port = up.port; + c->path = up.path; + + up.host = NULL; + up.path = NULL; + + int cr = esp_transport_connect(c->transport, c->host, c->port, c->timeout_ms); + if (cr < 0) { + c->state = NOSTR_WS_ERROR; + ws_client_destroy(c); + ws_free_url_parts(&up); + return NULL; + } + + int hs = esp_transport_ws_get_upgrade_request_status(c->transport); + if (hs != 101) { + c->state = NOSTR_WS_ERROR; + ws_client_destroy(c); + ws_free_url_parts(&up); + return NULL; + } + + c->state = NOSTR_WS_CONNECTED; + ws_free_url_parts(&up); + return c; +} + +int nostr_ws_close(nostr_ws_client_t* client) { + if (!client) return NOSTR_WS_ERROR_INVALID; + + if (client->state == NOSTR_WS_CONNECTED) { + (void)esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_CLOSE | WS_TRANSPORT_OPCODES_FIN), + NULL, + 0, + client->timeout_ms); + } + + client->state = NOSTR_WS_CLOSED; + ws_client_destroy(client); + return NOSTR_WS_SUCCESS; +} + +nostr_ws_state_t nostr_ws_get_state(nostr_ws_client_t* client) { + if (!client) return NOSTR_WS_ERROR; + return client->state; +} + +int nostr_ws_send_text(nostr_ws_client_t* client, const char* message) { + if (!client || !message || client->state != NOSTR_WS_CONNECTED) { + return NOSTR_WS_ERROR_INVALID; + } + + int len = (int)strlen(message); + int wr = esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_TEXT | WS_TRANSPORT_OPCODES_FIN), + message, + len, + client->timeout_ms); + return (wr == len) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK; +} + +int nostr_ws_receive(nostr_ws_client_t* client, char* buffer, size_t buffer_size, int timeout_ms) { + if (!client || !buffer || buffer_size == 0) return NOSTR_WS_ERROR_INVALID; + if (client->state != NOSTR_WS_CONNECTED && client->state != NOSTR_WS_CLOSING) { + return NOSTR_WS_ERROR_INVALID; + } + + int tmo = (timeout_ms > 0) ? timeout_ms : client->timeout_ms; + int n = esp_transport_read(client->transport, buffer, (int)buffer_size - 1, tmo); + if (n < 0) { + return NOSTR_WS_ERROR_NETWORK; + } + if (n == 0) { + /* timeout or internally-handled control frame; not a hard network failure */ + buffer[0] = '\0'; + return 0; + } + + ws_transport_opcodes_t opcode = esp_transport_ws_get_read_opcode(client->transport); + + if (opcode == WS_TRANSPORT_OPCODES_TEXT || opcode == WS_TRANSPORT_OPCODES_BINARY) { + buffer[n] = '\0'; + return n; + } + + if (opcode == WS_TRANSPORT_OPCODES_PING) { + (void)esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PONG | WS_TRANSPORT_OPCODES_FIN), + buffer, + n, + tmo); + return nostr_ws_receive(client, buffer, buffer_size, timeout_ms); + } + + if (opcode == WS_TRANSPORT_OPCODES_PONG) { + buffer[n] = '\0'; + return n; + } + + if (opcode == WS_TRANSPORT_OPCODES_CLOSE) { + client->state = NOSTR_WS_CLOSING; + buffer[0] = '\0'; + return 0; + } + + return NOSTR_WS_ERROR_PROTOCOL; +} + +int nostr_ws_ping(nostr_ws_client_t* client) { + if (!client || client->state != NOSTR_WS_CONNECTED) { + return NOSTR_WS_ERROR_INVALID; + } + + int wr = esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PING | WS_TRANSPORT_OPCODES_FIN), + "ping", + 4, + client->timeout_ms); + return (wr == 4) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK; +} + +int nostr_ws_set_timeout(nostr_ws_client_t* client, int timeout_ms) { + if (!client || timeout_ms < 0) return NOSTR_WS_ERROR_INVALID; + client->timeout_ms = timeout_ms; + return NOSTR_WS_SUCCESS; +} + +int nostr_relay_send_req(nostr_ws_client_t* client, const char* subscription_id, cJSON* filters) { + if (!client || !subscription_id || !filters) { + return NOSTR_WS_ERROR_INVALID; + } + + cJSON* req_array = cJSON_CreateArray(); + cJSON_AddItemToArray(req_array, cJSON_CreateString("REQ")); + cJSON_AddItemToArray(req_array, cJSON_CreateString(subscription_id)); + + if (cJSON_IsArray(filters)) { + cJSON* filter; + cJSON_ArrayForEach(filter, filters) { + cJSON_AddItemToArray(req_array, cJSON_Duplicate(filter, 1)); + } + } else { + cJSON_AddItemToArray(req_array, cJSON_Duplicate(filters, 1)); + } + + char* req_string = cJSON_PrintUnformatted(req_array); + if (!req_string) { + cJSON_Delete(req_array); + return NOSTR_WS_ERROR_MEMORY; + } + + int result = nostr_ws_send_text(client, req_string); + + free(req_string); + cJSON_Delete(req_array); + + return result; +} + +int nostr_relay_send_event(nostr_ws_client_t* client, cJSON* event) { + if (!client || !event) { + return NOSTR_WS_ERROR_INVALID; + } + + cJSON* event_array = cJSON_CreateArray(); + cJSON_AddItemToArray(event_array, cJSON_CreateString("EVENT")); + cJSON_AddItemToArray(event_array, cJSON_Duplicate(event, 1)); + + char* event_string = cJSON_PrintUnformatted(event_array); + if (!event_string) { + cJSON_Delete(event_array); + return NOSTR_WS_ERROR_MEMORY; + } + + int result = nostr_ws_send_text(client, event_string); + + free(event_string); + cJSON_Delete(event_array); + + return result; +} + +int nostr_relay_send_close(nostr_ws_client_t* client, const char* subscription_id) { + if (!client || !subscription_id) { + return NOSTR_WS_ERROR_INVALID; + } + + cJSON* close_array = cJSON_CreateArray(); + cJSON_AddItemToArray(close_array, cJSON_CreateString("CLOSE")); + cJSON_AddItemToArray(close_array, cJSON_CreateString(subscription_id)); + + char* close_string = cJSON_PrintUnformatted(close_array); + if (!close_string) { + cJSON_Delete(close_array); + return NOSTR_WS_ERROR_MEMORY; + } + + int result = nostr_ws_send_text(client, close_string); + + free(close_string); + cJSON_Delete(close_array); + + return result; +} + +int nostr_parse_relay_message(const char* message, char** message_type, cJSON** parsed_json) { + if (!message || !message_type || !parsed_json) { + return NOSTR_WS_ERROR_INVALID; + } + + *message_type = NULL; + *parsed_json = NULL; + + cJSON* json = cJSON_Parse(message); + if (!json || !cJSON_IsArray(json)) { + cJSON_Delete(json); + return NOSTR_WS_ERROR_PROTOCOL; + } + + cJSON* type_item = cJSON_GetArrayItem(json, 0); + if (!type_item || !cJSON_IsString(type_item)) { + cJSON_Delete(json); + return NOSTR_WS_ERROR_PROTOCOL; + } + + *message_type = strdup(type_item->valuestring); + if (!*message_type) { + cJSON_Delete(json); + return NOSTR_WS_ERROR_MEMORY; + } + + *parsed_json = json; + return NOSTR_WS_SUCCESS; +} + +const char* nostr_ws_strerror(int error_code) { + switch (error_code) { + case NOSTR_WS_SUCCESS: return "Success"; + case NOSTR_WS_ERROR_INVALID: return "Invalid parameter"; + case NOSTR_WS_ERROR_NETWORK: return "Network error"; + case NOSTR_WS_ERROR_PROTOCOL: return "Protocol error"; + case NOSTR_WS_ERROR_MEMORY: return "Memory allocation error"; + case NOSTR_WS_ERROR_TLS: return "TLS error"; + default: return "Unknown error"; + } +} diff --git a/platform/linux.c b/platform/linux.c new file mode 100644 index 00000000..af713ed7 --- /dev/null +++ b/platform/linux.c @@ -0,0 +1,28 @@ +#include "../nostr_core/nostr_platform.h" + +#include +#include + +int nostr_platform_random(unsigned char *buf, size_t len) { + if (buf == NULL || len == 0) { + return -1; + } + + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + return -1; + } + + size_t total = 0; + while (total < len) { + ssize_t n = read(fd, buf + total, len - total); + if (n <= 0) { + close(fd); + return -1; + } + total += (size_t)n; + } + + close(fd); + return 0; +} diff --git a/rewrite_mirror/HEAD b/rewrite_mirror/HEAD new file mode 100644 index 00000000..cb089cd8 --- /dev/null +++ b/rewrite_mirror/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/rewrite_mirror/config b/rewrite_mirror/config new file mode 100644 index 00000000..b4bd6b74 --- /dev/null +++ b/rewrite_mirror/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true +[remote "origin"] + url = git@git.laantungir.net:laantungir/nostr_core_lib.git + fetch = +refs/heads/*:refs/remotes/origin/* diff --git a/rewrite_mirror/description b/rewrite_mirror/description new file mode 100644 index 00000000..498b267a --- /dev/null +++ b/rewrite_mirror/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/rewrite_mirror/filter-repo/already_ran b/rewrite_mirror/filter-repo/already_ran new file mode 100644 index 00000000..db0c3278 --- /dev/null +++ b/rewrite_mirror/filter-repo/already_ran @@ -0,0 +1,3 @@ +This file exists to allow you to filter again without --force, +and to specify that metadata files should be updated instead +of rewritten \ No newline at end of file diff --git a/rewrite_mirror/filter-repo/changed-refs b/rewrite_mirror/filter-repo/changed-refs new file mode 100644 index 00000000..1d0fa09c --- /dev/null +++ b/rewrite_mirror/filter-repo/changed-refs @@ -0,0 +1,65 @@ +refs/tags/v0.1.0 +refs/tags/v0.1.1 +refs/tags/v0.1.10 +refs/tags/v0.1.11 +refs/tags/v0.1.12 +refs/tags/v0.1.13 +refs/tags/v0.1.14 +refs/tags/v0.1.15 +refs/tags/v0.1.16 +refs/tags/v0.1.17 +refs/tags/v0.1.18 +refs/tags/v0.1.19 +refs/tags/v0.1.2 +refs/tags/v0.1.20 +refs/tags/v0.1.21 +refs/tags/v0.1.22 +refs/tags/v0.1.23 +refs/tags/v0.1.24 +refs/tags/v0.1.25 +refs/tags/v0.1.26 +refs/tags/v0.1.27 +refs/tags/v0.1.28 +refs/tags/v0.1.29 +refs/tags/v0.1.3 +refs/tags/v0.1.30 +refs/tags/v0.1.31 +refs/tags/v0.1.32 +refs/tags/v0.1.33 +refs/tags/v0.1.4 +refs/tags/v0.1.5 +refs/tags/v0.1.6 +refs/tags/v0.1.7 +refs/tags/v0.1.8 +refs/tags/v0.1.9 +refs/tags/v0.2.0 +refs/tags/v0.2.1 +refs/tags/v0.2.2 +refs/tags/v0.2.3 +refs/tags/v0.2.4 +refs/tags/v0.2.5 +refs/tags/v0.2.6 +refs/tags/v0.4.10 +refs/tags/v0.4.11 +refs/tags/v0.4.12 +refs/tags/v0.4.13 +refs/tags/v0.4.3 +refs/tags/v0.4.4 +refs/tags/v0.4.5 +refs/tags/v0.4.6 +refs/tags/v0.4.7 +refs/tags/v0.4.8 +refs/tags/v0.4.9 +refs/tags/v0.5.0 +refs/tags/v0.5.1 +refs/tags/v0.5.10 +refs/tags/v0.5.11 +refs/tags/v0.5.12 +refs/tags/v0.5.2 +refs/tags/v0.5.3 +refs/tags/v0.5.4 +refs/tags/v0.5.5 +refs/tags/v0.5.6 +refs/tags/v0.5.7 +refs/tags/v0.5.8 +refs/tags/v0.5.9 diff --git a/rewrite_mirror/filter-repo/commit-map b/rewrite_mirror/filter-repo/commit-map new file mode 100644 index 00000000..efea16e5 --- /dev/null +++ b/rewrite_mirror/filter-repo/commit-map @@ -0,0 +1,102 @@ +old new +00df0cad99f2689df5bf0470e085a37ea31a6b26 7bf39a7e1071b7c00bb2af90c2c1d4fd911aec45 +05fe1df8aae540842ee9d86adb269407f65df4aa ec5a97f95139110230214a4636347e4fe71c1671 +0a8a813fcf3784b4e1da58a7b32cead4ac6699be 84257b2f5865e1963251003c527286125bb549c7 +0ace93e3030701a1a0085475d626d1bdce11cbe1 f6c8ef62465c110bdf1ebfa383647d5e59432fb6 +0d910ca18124f06e5840999373723b31796616e8 9bf74e76ce9e8e8c86eaf731d8e36f153972bea1 +0f897ab1b34b224503843770daf1ae85e2e8c0ff 2224448d12378601af19a0e842e36ea7a10ebfc6 +13ad24b676e307be79fac71bae7089c66cc65b23 13ad24b676e307be79fac71bae7089c66cc65b23 +13fd364ca313f475b18e6187ce71406eddf69d42 fcb5abdccfbaca22c2db8caf3074a52d3d7a8b9a +19452f45c28484a3fd345037a6e8892ad4392383 9879457d7e1a24a11518ce8a19583d91ecffbeef +1a549afa19963831af194c8c755ecd789e1c16dd 81814e8518d5ac9a1060317ec36755187b1cd3ea +1da4f6751ea6e275ece6226b13806de630abf482 64aee10a21447f0e839c2d32ead947a6af61b026 +20006df228cf4320d292960400137303c36c1524 98094cac8612ca4c8cc600fe476f87ad9356c3fd +2036d0165b7b1273bdec0631369f18e8a05b127e 3f5024c24c6865ad5eee06566a61912f9c3e6e3b +23c2a58c2b0562b2b9b272ecef67f35f748ccd8b 2c128a6295a790e5915b7cc77f7a6716bbdb5e1d +23e68bdc83f5ddd49fc5e70b948b6944627f7a97 df50ca1273b47ddf2c5695336118651488225619 +25bdce73376ee29bdda65959bd929dc74655b4a3 25bdce73376ee29bdda65959bd929dc74655b4a3 +2637378bf0988323f0a785811baa485da8464e47 2de890da5ccc11255ba8b6e210f098a3566be1a1 +2fe36acde41e128ec1a297aec0b777d81206b6e1 29d81f9551e6fdc8faf22c5b1b27fbfe26cf9a9b +303013a518ac564997707efde79bfb30f428e789 b512be79aba5e10cafad60d8b8acdffa4d4e8b94 +33129d82fdce8cff280bc0b5ba7ed5e49531606d 2f3140d4f6243b134d64b44d242bc27931fab066 +392b8632923a624e0bbc6ec3a14c0be088c86538 392b8632923a624e0bbc6ec3a14c0be088c86538 +39ebfcd90afd4f215db8458e79d0053234389c16 afe3e4787f0e3bbfa493c1cf94372016ab52c9a5 +3aaa46bb9b98649111ecbbbb042d34707d64e659 cfde3bcb0fb493c6a7cbd4fbfe87ef47b6cceda2 +3d2537603c56b0035af172aee7a2ccc6f4d9033a e8c0fcdf634f0de320675499486db729661f757b +3ebfdc06c09054d950482fb0107be02eedddac0d a9a1aff68248d37cc191939f15a37a31762ddd2b +40dd3aa20b1ad45ac9c657d78a342ab02df4efd1 8bcf76387bceb1816bf4c7a57e7fde693b208ee7 +445ab7a8f42c07167a4f78917cb9228003fd837f 0f72d7433a875a15c163baaea4030f837498b9ad +45fb6d061d90fc55bae2b465df94b31b84d53d7f 24903b8c90299066fea6d3e94040d42f9fd2c607 +481bf3158d041089e7756519cddd387af1e9b536 5c51ab1f9f72ea727e21249a1a8b8321784b85f9 +499accf44071da5748b27493026f5ffc309fd269 4e06d8f723a07b77a3558a1dd5a768d18bd282cb +4d0d794a9a930fdab146dceeb4090305c5218257 6c6bcba650e8805279fffe4427a67051aaa89f9a +4dc753cdccc31a5abb8f6e9fab04cf6ff9ce919e 2ce2ecbbfbbbf8ed95e6a2580f8d5dcf2c55e2a7 +54a6044083ac075c76ea02a90cb308606a50ed09 cdeb569a16940242ac790eb54e17b12ad2f0b519 +55e2a9c68e449ac375d1bdbb72a2bcf3e6eec9f3 0af77dffab1e6002f3ca990a18a30af7b894ba08 +58cabadc4466c916923c1002533e2c09583013dc 3533c874eaf1ab0c51f9d27bbd38b387ec1eed1f +5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 +5afe802e8fbfa79c3a6204472e3893b4c593a96d c87f8db6d841f9f13dab53981364637b0045ee36 +6014a250ddebc45c3562f6f7914b221323e5194a 52e5d300fc9552975b95537430d12512a8c6f5c9 +63972d974d40d0d0e34afcc69f378160e1d83193 e3b92480047469d28c2cd898f68b1a776c8c0ed4 +64b7c7ec33fb5eb12477a62b3fbc19683aaf3573 57ecbf0c116ac58fec15621a3a98cbd61f5cf1c0 +654bdd218649d0fb98ef2f74ba681630259e9a20 654bdd218649d0fb98ef2f74ba681630259e9a20 +669793dd677411ec41bcfb8206574052be4899f0 f07433578a78167a9a96f284331d905d1d395db8 +693e21423eff56ee6456900cda9812ca7aadb37c 693e21423eff56ee6456900cda9812ca7aadb37c +6b95ad37c5cd7f4d128802ead1e5da273d02e65c d7a8aa1dc4ad9021b49cadc1726418e660a0c00d +6d7b709f9a047668bb251bb175e9d0d2d9389b4a f86fb256b209ae9c4b5d0a546f93c9f2258bb5a4 +6ee62e6575d68226512910b301511c448bfe56f7 784cfc09c3a09f93d354801ee3f71245d3704567 +711a7cc15cbb0b7094e923987cb7e16695b2fc7f 6722d09579b9b5b03fa5ddd892eb3ffdacc4c2b3 +76e883fad4fab7bce85873ac645dec841358d01d 241ca036ed029e73c99103d5f805d7b76a3f9304 +76f9d11e069a0ecb58bbc462270e3d150ba28186 d384ade7902b8a9f81c5280ae5bff890afc9fd2f +77186c88dd6a440b8fdfac25e91f2342f1ff5f60 553774fdc47734907fc1967c7034fcbf30cb1b79 +77d92dbcf9f59d08bbf96f9dd42fcdca048b9154 4a096ff35f35c3877839bfd44dcba11ae0a839cf +77fadc2683e1ed9cbb8b790420f7201adfdb0f0f 8c610b6e369df848c553edbc882ec4ef5a269bbf +7a63f0db57814dc4e467972131ce55b4c4aa0c89 7a63f0db57814dc4e467972131ce55b4c4aa0c89 +8585e7649c3bd21f21c7bc6f7934269e36b5737c d7e43328bb2ba635a227347db029146aa449fcbe +8c5562edb09722075c8c039ec9040030447b9f40 8c5562edb09722075c8c039ec9040030447b9f40 +8ed9262c6582e2980d643e5312a1ada0029c8be5 ea9b8918f65aa28ac55b0e31557a472ed17d53ba +9191d446d3a737883ec0032df30e1ed2d05336c4 a2b700cf40ebce4b9c05d561b2271d04aa1a83f6 +98a802552b365c2d2a4b81736396d0bb179e9583 9913f92c960d70f7ce4df47e2a7b97a275739fa2 +98cfd81b01c181f98e00b7ffea7602ff2bfb390d 363b7e9adfc0ead83aba054a86a897b01c0a508a +993ca0f2a16c33388efc8c9d0cf4326147c230ea 3cfddbe75b2bc10626bfb6a5092b763a178951b4 +9a3965243c5d33136c464f867583bbbc5f08304a fca1a8077da44654d025253efc257ea3adc73dfc +9a63550863925ba64340240c2c7d89e2f82bc5ff d52ac9c10695bbae7960000512fad6df8cee8762 +9fd4c61df78be7ee8a92b940650cb8c195430ced dad1f5aa5260cdb7f395f88851f5df158b3cb6fe +a3a68f0fde9aaf8a3fadee4a250d4abe94202400 852ff337e57b311c072e3bccf0f28799489d2232 +a3f7a1bbfd22d0a16ed5fcd0b3c54d79612bcb35 2b80abc3ae6b3060e5ea06316529df88e9ed2276 +a595747affcbc5a20b476c76c414b8f2dbc59c1e c81db98e3f25c56a808b6c7b3b4df6f4355966dd +a8dc2ed0464f4220ef306fa1ce07673dd4e94880 0b2cca65490eef2c43ed7bd5bb6534c18de488c8 +adc7aa9dcfd16e41eb38c9883d64cfc11dd374bc 79d98587d3d25f4f8778b97e8db9244195c729a8 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e +ae672b7047ffdda81bbd8513900e8c80d0996fa7 7aea4b07df314bbf43a4f55b12c764b5141982e2 +af2117b57474c0757c7bc4c448b5053477019d81 01d382c0d3f08b824e5283c246e98457ca2e6737 +b3110218fc0c9118fd335d7ff481bc35ad1beaa4 b3110218fc0c9118fd335d7ff481bc35ad1beaa4 +ba97b1a6e38514713c0ec790f3170f79337a4d8c ba97b1a6e38514713c0ec790f3170f79337a4d8c +c0784fc890744e31816cd4a208130015f8302d3e c6896eebb4047d5fe5c61d589d373fff211d8284 +c0d095e57b11c3065fe4acc9c21245cfec1e6a28 31947d12b735396e2c202f8dced51736ffe80106 +c109c93382a07eed72c72026e07b9ca19b423f1e bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 +c3a948288207572bfb889ccdd21be0f740e12d62 997c3e976badbb7a4aad2561756e8f2fd1bd1940 +c569c0c34649cf3390e35e8ed0459efc2ed09f74 04f22e6f47d7ac161d88b0a6a440523d4cbfa788 +ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 +cb4ff939060ea67c06d70435f5e6a30745366256 92d5927392b6eaf8ca5a3aaa611a10ef946f71e8 +d257ae49f1062dc85fe4850209d0690d925327a8 a4ae8df66fe53dd6906cddbdd136e98a282faf0a +d6a0bd67b2a68fbfa586e8022d7dd3f2ceb1a1f3 7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 +d8b342ca3fd2547e5a14ae75b9b17602e9342b02 a4fbba027e6a6577a62e413e9078be0ae39fc2d0 +d8d3f9b28c0a8ae07b036a762f8cf00ec65f9860 3186c2ead6bba56da5fc8addf4851c2eaab664e4 +df23fd618a157804129308b4f872b227a6225caf fdc7cc2a383abbde2a26c1d5cf30b7a803de1637 +e115d2e03f5d28b993c64da1663933c4e4cde129 e115d2e03f5d28b993c64da1663933c4e4cde129 +e137560d6443b564ffab42c1ed0c7dbc758e3065 3b076ea372bcd24f94e848faba01e1d50535ce3a +e21c8eb5be4bd7d0b0c90c1d3f9fe3c9bfd7396f d264d9f5ed96641abacb2435b10e56b0667b3f6b +e40f3037d3f7547d8da5b35e3ff9d2ff24553806 9e9fcba96cd38aca50d67e6a74272f14f13e48c1 +e77973aea06d2d28de621f828ca5d098eb16e752 c871c402197880af9cf6fcf9b70512d4effef405 +ea40aec6b867168625edaaad2e1aece5d2069293 af4f7cace95206873830de6d4cf72d59bb2d3b61 +ea84795911d70c10a635eae63b102b6ad83ed8b9 2b3113dafc9cfe4104a9fdfbf873b69d89ec2848 +eb7a9e6098aa684f3af13ae8f11e175e080d41a0 e087a17eb927a754da1d70fc48d36dd8f32f8429 +ec8eb5555b5679dc15cb867f9623c8909d97b153 b0a2279bcd59ddeef80bec22817ce61af5a7d099 +ef8e68b952ca9d3499e37d9607fc4e44dac8a369 ef8e68b952ca9d3499e37d9607fc4e44dac8a369 +f1c22bdf530afae20fc258ad990b430543cb6895 dfe8a20e2e97feb67a7506e22c0c3ba766f22f2c +f3068f82f3ba5f9f8680af33a1081a9d6e92810d e87d7364ff2d5d8011d77588532fbf653d09891d +f470759b960f4bcf06e7e328c564786daa479ae0 4c09cf2b266682253341206978b18d379c77cf87 +f50384962d4bf357b83335588b881ac74dacae3b ace0b5a7927f99e701c28b0a5d5623ac139cb7dc +f7e179dd85e51ec4f4fe801d1280f73f84643cf8 5760a3a7d0660cd58aa8e99919a71e187da4f785 +fb3c9dbc76754730b0de6b07b721b76f745b4c4b 2a861298da59c45da7e7628b53acc0bb8278582e diff --git a/rewrite_mirror/filter-repo/first-changed-commits b/rewrite_mirror/filter-repo/first-changed-commits new file mode 100644 index 00000000..f1e333fc --- /dev/null +++ b/rewrite_mirror/filter-repo/first-changed-commits @@ -0,0 +1 @@ +ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 diff --git a/rewrite_mirror/filter-repo/ref-map b/rewrite_mirror/filter-repo/ref-map new file mode 100644 index 00000000..3f628d3a --- /dev/null +++ b/rewrite_mirror/filter-repo/ref-map @@ -0,0 +1,76 @@ +old new ref +e115d2e03f5d28b993c64da1663933c4e4cde129 e115d2e03f5d28b993c64da1663933c4e4cde129 refs/heads/master +ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.0 +ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.1 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.10 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.11 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.12 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.13 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.14 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.15 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.16 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.17 +3aaa46bb9b98649111ecbbbb042d34707d64e659 cfde3bcb0fb493c6a7cbd4fbfe87ef47b6cceda2 refs/tags/v0.1.18 +5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.19 +ca6b4754f9c57aba7788a55d83eedbe4127943ed 6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.2 +5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.20 +5a7c79687399d1ace25fbb3036e9ada43b5a5d1f ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.21 +c109c93382a07eed72c72026e07b9ca19b423f1e bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.22 +c109c93382a07eed72c72026e07b9ca19b423f1e bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.23 +2637378bf0988323f0a785811baa485da8464e47 2de890da5ccc11255ba8b6e210f098a3566be1a1 refs/tags/v0.1.24 +77fadc2683e1ed9cbb8b790420f7201adfdb0f0f 8c610b6e369df848c553edbc882ec4ef5a269bbf refs/tags/v0.1.25 +f1c22bdf530afae20fc258ad990b430543cb6895 dfe8a20e2e97feb67a7506e22c0c3ba766f22f2c refs/tags/v0.1.26 +9191d446d3a737883ec0032df30e1ed2d05336c4 a2b700cf40ebce4b9c05d561b2271d04aa1a83f6 refs/tags/v0.1.27 +d6a0bd67b2a68fbfa586e8022d7dd3f2ceb1a1f3 7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.28 +d6a0bd67b2a68fbfa586e8022d7dd3f2ceb1a1f3 7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.29 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.3 +3d2537603c56b0035af172aee7a2ccc6f4d9033a e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.30 +3d2537603c56b0035af172aee7a2ccc6f4d9033a e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.31 +c569c0c34649cf3390e35e8ed0459efc2ed09f74 04f22e6f47d7ac161d88b0a6a440523d4cbfa788 refs/tags/v0.1.32 +f50384962d4bf357b83335588b881ac74dacae3b ace0b5a7927f99e701c28b0a5d5623ac139cb7dc refs/tags/v0.1.33 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.4 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.5 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.6 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.7 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.8 +ae4aa7cf80dd1215a4c93467dee327ea897e8635 3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.9 +1da4f6751ea6e275ece6226b13806de630abf482 64aee10a21447f0e839c2d32ead947a6af61b026 refs/tags/v0.2.0 +77d92dbcf9f59d08bbf96f9dd42fcdca048b9154 4a096ff35f35c3877839bfd44dcba11ae0a839cf refs/tags/v0.2.1 +e40f3037d3f7547d8da5b35e3ff9d2ff24553806 9e9fcba96cd38aca50d67e6a74272f14f13e48c1 refs/tags/v0.2.2 +c0d095e57b11c3065fe4acc9c21245cfec1e6a28 31947d12b735396e2c202f8dced51736ffe80106 refs/tags/v0.2.3 +445ab7a8f42c07167a4f78917cb9228003fd837f 0f72d7433a875a15c163baaea4030f837498b9ad refs/tags/v0.2.4 +9a63550863925ba64340240c2c7d89e2f82bc5ff d52ac9c10695bbae7960000512fad6df8cee8762 refs/tags/v0.2.5 +0d910ca18124f06e5840999373723b31796616e8 9bf74e76ce9e8e8c86eaf731d8e36f153972bea1 refs/tags/v0.2.6 +23e68bdc83f5ddd49fc5e70b948b6944627f7a97 df50ca1273b47ddf2c5695336118651488225619 refs/tags/v0.4.10 +481bf3158d041089e7756519cddd387af1e9b536 5c51ab1f9f72ea727e21249a1a8b8321784b85f9 refs/tags/v0.4.11 +1a549afa19963831af194c8c755ecd789e1c16dd 81814e8518d5ac9a1060317ec36755187b1cd3ea refs/tags/v0.4.12 +adc7aa9dcfd16e41eb38c9883d64cfc11dd374bc 79d98587d3d25f4f8778b97e8db9244195c729a8 refs/tags/v0.4.13 +54a6044083ac075c76ea02a90cb308606a50ed09 cdeb569a16940242ac790eb54e17b12ad2f0b519 refs/tags/v0.4.3 +6b95ad37c5cd7f4d128802ead1e5da273d02e65c d7a8aa1dc4ad9021b49cadc1726418e660a0c00d refs/tags/v0.4.4 +499accf44071da5748b27493026f5ffc309fd269 4e06d8f723a07b77a3558a1dd5a768d18bd282cb refs/tags/v0.4.5 +c0784fc890744e31816cd4a208130015f8302d3e c6896eebb4047d5fe5c61d589d373fff211d8284 refs/tags/v0.4.6 +23c2a58c2b0562b2b9b272ecef67f35f748ccd8b 2c128a6295a790e5915b7cc77f7a6716bbdb5e1d refs/tags/v0.4.7 +f3068f82f3ba5f9f8680af33a1081a9d6e92810d e87d7364ff2d5d8011d77588532fbf653d09891d refs/tags/v0.4.8 +76f9d11e069a0ecb58bbc462270e3d150ba28186 d384ade7902b8a9f81c5280ae5bff890afc9fd2f refs/tags/v0.4.9 +98cfd81b01c181f98e00b7ffea7602ff2bfb390d 363b7e9adfc0ead83aba054a86a897b01c0a508a refs/tags/v0.5.0 +ea40aec6b867168625edaaad2e1aece5d2069293 af4f7cace95206873830de6d4cf72d59bb2d3b61 refs/tags/v0.5.1 +fb3c9dbc76754730b0de6b07b721b76f745b4c4b 2a861298da59c45da7e7628b53acc0bb8278582e refs/tags/v0.5.10 +f470759b960f4bcf06e7e328c564786daa479ae0 4c09cf2b266682253341206978b18d379c77cf87 refs/tags/v0.5.11 +ec8eb5555b5679dc15cb867f9623c8909d97b153 b0a2279bcd59ddeef80bec22817ce61af5a7d099 refs/tags/v0.5.12 +b3110218fc0c9118fd335d7ff481bc35ad1beaa4 b3110218fc0c9118fd335d7ff481bc35ad1beaa4 refs/tags/v0.5.13 +25bdce73376ee29bdda65959bd929dc74655b4a3 25bdce73376ee29bdda65959bd929dc74655b4a3 refs/tags/v0.5.14 +ef8e68b952ca9d3499e37d9607fc4e44dac8a369 ef8e68b952ca9d3499e37d9607fc4e44dac8a369 refs/tags/v0.5.15 +8c5562edb09722075c8c039ec9040030447b9f40 8c5562edb09722075c8c039ec9040030447b9f40 refs/tags/v0.5.16 +0a8a813fcf3784b4e1da58a7b32cead4ac6699be 84257b2f5865e1963251003c527286125bb549c7 refs/tags/v0.5.2 +ae672b7047ffdda81bbd8513900e8c80d0996fa7 7aea4b07df314bbf43a4f55b12c764b5141982e2 refs/tags/v0.5.3 +4d0d794a9a930fdab146dceeb4090305c5218257 6c6bcba650e8805279fffe4427a67051aaa89f9a refs/tags/v0.5.4 +39ebfcd90afd4f215db8458e79d0053234389c16 afe3e4787f0e3bbfa493c1cf94372016ab52c9a5 refs/tags/v0.5.5 +a595747affcbc5a20b476c76c414b8f2dbc59c1e c81db98e3f25c56a808b6c7b3b4df6f4355966dd refs/tags/v0.5.6 +a3f7a1bbfd22d0a16ed5fcd0b3c54d79612bcb35 2b80abc3ae6b3060e5ea06316529df88e9ed2276 refs/tags/v0.5.7 +f7e179dd85e51ec4f4fe801d1280f73f84643cf8 5760a3a7d0660cd58aa8e99919a71e187da4f785 refs/tags/v0.5.8 +303013a518ac564997707efde79bfb30f428e789 b512be79aba5e10cafad60d8b8acdffa4d4e8b94 refs/tags/v0.5.9 +392b8632923a624e0bbc6ec3a14c0be088c86538 392b8632923a624e0bbc6ec3a14c0be088c86538 refs/tags/v0.6.0 +654bdd218649d0fb98ef2f74ba681630259e9a20 654bdd218649d0fb98ef2f74ba681630259e9a20 refs/tags/v0.6.1 +693e21423eff56ee6456900cda9812ca7aadb37c 693e21423eff56ee6456900cda9812ca7aadb37c refs/tags/v0.6.2 +ba97b1a6e38514713c0ec790f3170f79337a4d8c ba97b1a6e38514713c0ec790f3170f79337a4d8c refs/tags/v0.6.3 +7a63f0db57814dc4e467972131ce55b4c4aa0c89 7a63f0db57814dc4e467972131ce55b4c4aa0c89 refs/tags/v0.6.4 diff --git a/rewrite_mirror/filter-repo/suboptimal-issues b/rewrite_mirror/filter-repo/suboptimal-issues new file mode 100644 index 00000000..701e20c5 --- /dev/null +++ b/rewrite_mirror/filter-repo/suboptimal-issues @@ -0,0 +1 @@ +No filtering problems encountered. diff --git a/rewrite_mirror/hooks/applypatch-msg.sample b/rewrite_mirror/hooks/applypatch-msg.sample new file mode 100755 index 00000000..a5d7b84a --- /dev/null +++ b/rewrite_mirror/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/rewrite_mirror/hooks/commit-msg.sample b/rewrite_mirror/hooks/commit-msg.sample new file mode 100755 index 00000000..b58d1184 --- /dev/null +++ b/rewrite_mirror/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/rewrite_mirror/hooks/fsmonitor-watchman.sample b/rewrite_mirror/hooks/fsmonitor-watchman.sample new file mode 100755 index 00000000..23e856f5 --- /dev/null +++ b/rewrite_mirror/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/rewrite_mirror/hooks/post-update.sample b/rewrite_mirror/hooks/post-update.sample new file mode 100755 index 00000000..ec17ec19 --- /dev/null +++ b/rewrite_mirror/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/rewrite_mirror/hooks/pre-applypatch.sample b/rewrite_mirror/hooks/pre-applypatch.sample new file mode 100755 index 00000000..4142082b --- /dev/null +++ b/rewrite_mirror/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/rewrite_mirror/hooks/pre-commit.sample b/rewrite_mirror/hooks/pre-commit.sample new file mode 100755 index 00000000..29ed5ee4 --- /dev/null +++ b/rewrite_mirror/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff-index --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/rewrite_mirror/hooks/pre-merge-commit.sample b/rewrite_mirror/hooks/pre-merge-commit.sample new file mode 100755 index 00000000..399eab19 --- /dev/null +++ b/rewrite_mirror/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/rewrite_mirror/hooks/pre-push.sample b/rewrite_mirror/hooks/pre-push.sample new file mode 100755 index 00000000..4ce688d3 --- /dev/null +++ b/rewrite_mirror/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/rewrite_mirror/hooks/pre-rebase.sample b/rewrite_mirror/hooks/pre-rebase.sample new file mode 100755 index 00000000..6cbef5c3 --- /dev/null +++ b/rewrite_mirror/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/rewrite_mirror/hooks/pre-receive.sample b/rewrite_mirror/hooks/pre-receive.sample new file mode 100755 index 00000000..a1fd29ec --- /dev/null +++ b/rewrite_mirror/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/rewrite_mirror/hooks/prepare-commit-msg.sample b/rewrite_mirror/hooks/prepare-commit-msg.sample new file mode 100755 index 00000000..10fa14c5 --- /dev/null +++ b/rewrite_mirror/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/rewrite_mirror/hooks/push-to-checkout.sample b/rewrite_mirror/hooks/push-to-checkout.sample new file mode 100755 index 00000000..af5a0c00 --- /dev/null +++ b/rewrite_mirror/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/rewrite_mirror/hooks/update.sample b/rewrite_mirror/hooks/update.sample new file mode 100755 index 00000000..c4d426bc --- /dev/null +++ b/rewrite_mirror/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/rewrite_mirror/info/exclude b/rewrite_mirror/info/exclude new file mode 100644 index 00000000..a5196d1b --- /dev/null +++ b/rewrite_mirror/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/rewrite_mirror/info/refs b/rewrite_mirror/info/refs new file mode 100644 index 00000000..6fd78a5b --- /dev/null +++ b/rewrite_mirror/info/refs @@ -0,0 +1,75 @@ +e115d2e03f5d28b993c64da1663933c4e4cde129 refs/heads/master +6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.0 +6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.1 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.10 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.11 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.12 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.13 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.14 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.15 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.16 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.17 +cfde3bcb0fb493c6a7cbd4fbfe87ef47b6cceda2 refs/tags/v0.1.18 +ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.19 +6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.2 +ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.20 +ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.21 +bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.22 +bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.23 +2de890da5ccc11255ba8b6e210f098a3566be1a1 refs/tags/v0.1.24 +8c610b6e369df848c553edbc882ec4ef5a269bbf refs/tags/v0.1.25 +dfe8a20e2e97feb67a7506e22c0c3ba766f22f2c refs/tags/v0.1.26 +a2b700cf40ebce4b9c05d561b2271d04aa1a83f6 refs/tags/v0.1.27 +7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.28 +7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.29 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.3 +e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.30 +e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.31 +04f22e6f47d7ac161d88b0a6a440523d4cbfa788 refs/tags/v0.1.32 +ace0b5a7927f99e701c28b0a5d5623ac139cb7dc refs/tags/v0.1.33 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.4 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.5 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.6 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.7 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.8 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.9 +64aee10a21447f0e839c2d32ead947a6af61b026 refs/tags/v0.2.0 +4a096ff35f35c3877839bfd44dcba11ae0a839cf refs/tags/v0.2.1 +9e9fcba96cd38aca50d67e6a74272f14f13e48c1 refs/tags/v0.2.2 +31947d12b735396e2c202f8dced51736ffe80106 refs/tags/v0.2.3 +0f72d7433a875a15c163baaea4030f837498b9ad refs/tags/v0.2.4 +d52ac9c10695bbae7960000512fad6df8cee8762 refs/tags/v0.2.5 +9bf74e76ce9e8e8c86eaf731d8e36f153972bea1 refs/tags/v0.2.6 +df50ca1273b47ddf2c5695336118651488225619 refs/tags/v0.4.10 +5c51ab1f9f72ea727e21249a1a8b8321784b85f9 refs/tags/v0.4.11 +81814e8518d5ac9a1060317ec36755187b1cd3ea refs/tags/v0.4.12 +79d98587d3d25f4f8778b97e8db9244195c729a8 refs/tags/v0.4.13 +cdeb569a16940242ac790eb54e17b12ad2f0b519 refs/tags/v0.4.3 +d7a8aa1dc4ad9021b49cadc1726418e660a0c00d refs/tags/v0.4.4 +4e06d8f723a07b77a3558a1dd5a768d18bd282cb refs/tags/v0.4.5 +c6896eebb4047d5fe5c61d589d373fff211d8284 refs/tags/v0.4.6 +2c128a6295a790e5915b7cc77f7a6716bbdb5e1d refs/tags/v0.4.7 +e87d7364ff2d5d8011d77588532fbf653d09891d refs/tags/v0.4.8 +d384ade7902b8a9f81c5280ae5bff890afc9fd2f refs/tags/v0.4.9 +363b7e9adfc0ead83aba054a86a897b01c0a508a refs/tags/v0.5.0 +af4f7cace95206873830de6d4cf72d59bb2d3b61 refs/tags/v0.5.1 +2a861298da59c45da7e7628b53acc0bb8278582e refs/tags/v0.5.10 +4c09cf2b266682253341206978b18d379c77cf87 refs/tags/v0.5.11 +b0a2279bcd59ddeef80bec22817ce61af5a7d099 refs/tags/v0.5.12 +b3110218fc0c9118fd335d7ff481bc35ad1beaa4 refs/tags/v0.5.13 +25bdce73376ee29bdda65959bd929dc74655b4a3 refs/tags/v0.5.14 +ef8e68b952ca9d3499e37d9607fc4e44dac8a369 refs/tags/v0.5.15 +8c5562edb09722075c8c039ec9040030447b9f40 refs/tags/v0.5.16 +84257b2f5865e1963251003c527286125bb549c7 refs/tags/v0.5.2 +7aea4b07df314bbf43a4f55b12c764b5141982e2 refs/tags/v0.5.3 +6c6bcba650e8805279fffe4427a67051aaa89f9a refs/tags/v0.5.4 +afe3e4787f0e3bbfa493c1cf94372016ab52c9a5 refs/tags/v0.5.5 +c81db98e3f25c56a808b6c7b3b4df6f4355966dd refs/tags/v0.5.6 +2b80abc3ae6b3060e5ea06316529df88e9ed2276 refs/tags/v0.5.7 +5760a3a7d0660cd58aa8e99919a71e187da4f785 refs/tags/v0.5.8 +b512be79aba5e10cafad60d8b8acdffa4d4e8b94 refs/tags/v0.5.9 +392b8632923a624e0bbc6ec3a14c0be088c86538 refs/tags/v0.6.0 +654bdd218649d0fb98ef2f74ba681630259e9a20 refs/tags/v0.6.1 +693e21423eff56ee6456900cda9812ca7aadb37c refs/tags/v0.6.2 +ba97b1a6e38514713c0ec790f3170f79337a4d8c refs/tags/v0.6.3 +7a63f0db57814dc4e467972131ce55b4c4aa0c89 refs/tags/v0.6.4 diff --git a/rewrite_mirror/objects/info/commit-graph b/rewrite_mirror/objects/info/commit-graph new file mode 100644 index 00000000..6e33ebfb Binary files /dev/null and b/rewrite_mirror/objects/info/commit-graph differ diff --git a/rewrite_mirror/objects/info/packs b/rewrite_mirror/objects/info/packs new file mode 100644 index 00000000..03f13ce7 --- /dev/null +++ b/rewrite_mirror/objects/info/packs @@ -0,0 +1,2 @@ +P pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.pack + diff --git a/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.bitmap b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.bitmap new file mode 100644 index 00000000..a75a86c2 Binary files /dev/null and b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.bitmap differ diff --git a/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.idx b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.idx new file mode 100644 index 00000000..8f473475 Binary files /dev/null and b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.idx differ diff --git a/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.pack b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.pack new file mode 100644 index 00000000..10a4e751 Binary files /dev/null and b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.pack differ diff --git a/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.rev b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.rev new file mode 100644 index 00000000..8f908ce6 Binary files /dev/null and b/rewrite_mirror/objects/pack/pack-bd5c546d8e7dc280a8a4778dbb0b46596173b9ce.rev differ diff --git a/rewrite_mirror/packed-refs b/rewrite_mirror/packed-refs new file mode 100644 index 00000000..126c68b8 --- /dev/null +++ b/rewrite_mirror/packed-refs @@ -0,0 +1,76 @@ +# pack-refs with: peeled fully-peeled sorted +e115d2e03f5d28b993c64da1663933c4e4cde129 refs/heads/master +6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.0 +6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.1 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.10 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.11 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.12 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.13 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.14 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.15 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.16 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.17 +cfde3bcb0fb493c6a7cbd4fbfe87ef47b6cceda2 refs/tags/v0.1.18 +ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.19 +6369fb0cf3e7df34851913534a265878ac1e9828 refs/tags/v0.1.2 +ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.20 +ac85ab6f0799f6a0c209f3efba5feb39d95bafc3 refs/tags/v0.1.21 +bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.22 +bb673cc1a479e0d2a8f7bfef361b1c0bf3fade81 refs/tags/v0.1.23 +2de890da5ccc11255ba8b6e210f098a3566be1a1 refs/tags/v0.1.24 +8c610b6e369df848c553edbc882ec4ef5a269bbf refs/tags/v0.1.25 +dfe8a20e2e97feb67a7506e22c0c3ba766f22f2c refs/tags/v0.1.26 +a2b700cf40ebce4b9c05d561b2271d04aa1a83f6 refs/tags/v0.1.27 +7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.28 +7cf7ffe025adf43ee8ed90e48fdd7c080bd9f4b4 refs/tags/v0.1.29 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.3 +e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.30 +e8c0fcdf634f0de320675499486db729661f757b refs/tags/v0.1.31 +04f22e6f47d7ac161d88b0a6a440523d4cbfa788 refs/tags/v0.1.32 +ace0b5a7927f99e701c28b0a5d5623ac139cb7dc refs/tags/v0.1.33 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.4 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.5 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.6 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.7 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.8 +3108661b0a6f6ab33201bc6c9c405a161273978e refs/tags/v0.1.9 +64aee10a21447f0e839c2d32ead947a6af61b026 refs/tags/v0.2.0 +4a096ff35f35c3877839bfd44dcba11ae0a839cf refs/tags/v0.2.1 +9e9fcba96cd38aca50d67e6a74272f14f13e48c1 refs/tags/v0.2.2 +31947d12b735396e2c202f8dced51736ffe80106 refs/tags/v0.2.3 +0f72d7433a875a15c163baaea4030f837498b9ad refs/tags/v0.2.4 +d52ac9c10695bbae7960000512fad6df8cee8762 refs/tags/v0.2.5 +9bf74e76ce9e8e8c86eaf731d8e36f153972bea1 refs/tags/v0.2.6 +df50ca1273b47ddf2c5695336118651488225619 refs/tags/v0.4.10 +5c51ab1f9f72ea727e21249a1a8b8321784b85f9 refs/tags/v0.4.11 +81814e8518d5ac9a1060317ec36755187b1cd3ea refs/tags/v0.4.12 +79d98587d3d25f4f8778b97e8db9244195c729a8 refs/tags/v0.4.13 +cdeb569a16940242ac790eb54e17b12ad2f0b519 refs/tags/v0.4.3 +d7a8aa1dc4ad9021b49cadc1726418e660a0c00d refs/tags/v0.4.4 +4e06d8f723a07b77a3558a1dd5a768d18bd282cb refs/tags/v0.4.5 +c6896eebb4047d5fe5c61d589d373fff211d8284 refs/tags/v0.4.6 +2c128a6295a790e5915b7cc77f7a6716bbdb5e1d refs/tags/v0.4.7 +e87d7364ff2d5d8011d77588532fbf653d09891d refs/tags/v0.4.8 +d384ade7902b8a9f81c5280ae5bff890afc9fd2f refs/tags/v0.4.9 +363b7e9adfc0ead83aba054a86a897b01c0a508a refs/tags/v0.5.0 +af4f7cace95206873830de6d4cf72d59bb2d3b61 refs/tags/v0.5.1 +2a861298da59c45da7e7628b53acc0bb8278582e refs/tags/v0.5.10 +4c09cf2b266682253341206978b18d379c77cf87 refs/tags/v0.5.11 +b0a2279bcd59ddeef80bec22817ce61af5a7d099 refs/tags/v0.5.12 +b3110218fc0c9118fd335d7ff481bc35ad1beaa4 refs/tags/v0.5.13 +25bdce73376ee29bdda65959bd929dc74655b4a3 refs/tags/v0.5.14 +ef8e68b952ca9d3499e37d9607fc4e44dac8a369 refs/tags/v0.5.15 +8c5562edb09722075c8c039ec9040030447b9f40 refs/tags/v0.5.16 +84257b2f5865e1963251003c527286125bb549c7 refs/tags/v0.5.2 +7aea4b07df314bbf43a4f55b12c764b5141982e2 refs/tags/v0.5.3 +6c6bcba650e8805279fffe4427a67051aaa89f9a refs/tags/v0.5.4 +afe3e4787f0e3bbfa493c1cf94372016ab52c9a5 refs/tags/v0.5.5 +c81db98e3f25c56a808b6c7b3b4df6f4355966dd refs/tags/v0.5.6 +2b80abc3ae6b3060e5ea06316529df88e9ed2276 refs/tags/v0.5.7 +5760a3a7d0660cd58aa8e99919a71e187da4f785 refs/tags/v0.5.8 +b512be79aba5e10cafad60d8b8acdffa4d4e8b94 refs/tags/v0.5.9 +392b8632923a624e0bbc6ec3a14c0be088c86538 refs/tags/v0.6.0 +654bdd218649d0fb98ef2f74ba681630259e9a20 refs/tags/v0.6.1 +693e21423eff56ee6456900cda9812ca7aadb37c refs/tags/v0.6.2 +ba97b1a6e38514713c0ec790f3170f79337a4d8c refs/tags/v0.6.3 +7a63f0db57814dc4e467972131ce55b4c4aa0c89 refs/tags/v0.6.4 diff --git a/rewrite_mirror/refs/remotes/origin/master b/rewrite_mirror/refs/remotes/origin/master new file mode 100644 index 00000000..5995be1a --- /dev/null +++ b/rewrite_mirror/refs/remotes/origin/master @@ -0,0 +1 @@ +e115d2e03f5d28b993c64da1663933c4e4cde129 diff --git a/tests/async_publish_test b/tests/async_publish_test index 3711d9bc..95d37c00 100755 Binary files a/tests/async_publish_test and b/tests/async_publish_test differ diff --git a/tests/backward_compat_test b/tests/backward_compat_test index 898b66be..4a41675f 100755 Binary files a/tests/backward_compat_test and b/tests/backward_compat_test differ diff --git a/tests/bip32_test b/tests/bip32_test index 07c499bd..0b11f061 100755 Binary files a/tests/bip32_test and b/tests/bip32_test differ diff --git a/tests/blossom_client_live_test b/tests/blossom_client_live_test index ff957d4d..4852f95f 100755 Binary files a/tests/blossom_client_live_test and b/tests/blossom_client_live_test differ diff --git a/tests/blossom_client_test b/tests/blossom_client_test index 859462a8..af10b0b7 100755 Binary files a/tests/blossom_client_test and b/tests/blossom_client_test differ diff --git a/tests/blossom_mock_error_test b/tests/blossom_mock_error_test index f06606c9..14953503 100755 Binary files a/tests/blossom_mock_error_test and b/tests/blossom_mock_error_test differ diff --git a/tests/cashu_mint_test b/tests/cashu_mint_test index dc656bfc..66b19c7c 100755 Binary files a/tests/cashu_mint_test and b/tests/cashu_mint_test differ diff --git a/tests/chacha20_test b/tests/chacha20_test index 27a3c863..dda5c5da 100755 Binary files a/tests/chacha20_test and b/tests/chacha20_test differ diff --git a/tests/chacha20poly1305_test.c b/tests/chacha20poly1305_test.c new file mode 100644 index 00000000..ea4cb869 --- /dev/null +++ b/tests/chacha20poly1305_test.c @@ -0,0 +1,243 @@ +/* + * ChaCha20-Poly1305 / Poly1305 Test Suite - RFC 8439 vectors + */ + +#include +#include +#include +#include + +int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg, + size_t msg_len, unsigned char tag[16]); + +int nostr_chacha20poly1305_encrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *plaintext, size_t pt_len, + unsigned char *ciphertext, + unsigned char tag[16]); + +int nostr_chacha20poly1305_decrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *ciphertext, size_t ct_len, + const unsigned char tag[16], + unsigned char *plaintext); + +static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) { + size_t i; + if (!hex || !out) return -1; + for (i = 0; i < out_len; i++) { + if (sscanf(hex + (i * 2), "%2hhx", &out[i]) != 1) { + return -1; + } + } + return 0; +} + +static int bytes_equal(const uint8_t *a, const uint8_t *b, size_t len) { + return memcmp(a, b, len) == 0; +} + +static void print_hex(const uint8_t *buf, size_t len) { + size_t i; + for (i = 0; i < len; i++) { + printf("%02x", buf[i]); + } +} + +static int test_poly1305_rfc8439_2_5_2(void) { + const char *key_hex = + "85d6be7857556d337f4452fe42d506a8" + "0103808afb0db2fd4abff6af4149f51b"; + const char *msg = "Cryptographic Forum Research Group"; + const char *expected_tag_hex = "a8061dc1305136c6c22b8baf0c0127a9"; + + uint8_t key[32]; + uint8_t tag[16]; + uint8_t expected[16]; + + printf("=== Poly1305 RFC 8439 Β§2.5.2 ===\n"); + + if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 || + hex_to_bytes(expected_tag_hex, expected, sizeof(expected)) != 0) { + printf("❌ hex parse failed\n\n"); + return 0; + } + + if (nostr_poly1305_mac(key, (const unsigned char *)msg, strlen(msg), tag) != 0) { + printf("❌ nostr_poly1305_mac failed\n\n"); + return 0; + } + + if (!bytes_equal(tag, expected, sizeof(tag))) { + printf("❌ tag mismatch\nExpected: "); + print_hex(expected, sizeof(expected)); + printf("\nGot: "); + print_hex(tag, sizeof(tag)); + printf("\n\n"); + return 0; + } + + printf("βœ… passed\n\n"); + return 1; +} + +static int test_aead_rfc8439_appendix_a_5(void) { + const char *key_hex = + "1c9240a5eb55d38af333888604f6b5f0" + "473917c1402b80099dca5cbc207075c0"; + const char *nonce_hex = "000000000102030405060708"; + const char *aad_hex = "f33388860000000000004e91"; + const char *plaintext_hex = + "496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420" + "666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d61792062652075706461" + "7465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + "6e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f20757365" + "20496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f722074" + "6f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f6772" + "6573732e2fe2809d"; + const char *expected_ct_hex = + "64a0861575861af460f062c79be643bd" + "5e805cfd345cf389f108670ac76c8cb2" + "4c6cfc18755d43eea09ee94e382d26b0" + "bdb7b73c321b0100d4f03b7f355894cf" + "332f830e710b97ce98c8a84abd0b9481" + "14ad176e008d33bd60f982b1ff37c855" + "9797a06ef4f0ef61c186324e2b350638" + "3606907b6a7c02b0f9f6157b53c867e4" + "b9166c767b804d46a59b5216cde7a4e9" + "9040c5a40433225ee282a1b0a06c523e" + "af4534d7f83fa1155b0047718cbc546a" + "0d072b04b3564eea1b422273f548271a" + "0bb2316053fa76991955ebd63159434e" + "cebb4e466dae5a1073a6727627097a10" + "49e617d91d361094fa68f0ff77987130" + "305beaba2eda04df997b714d6c6f2c29" + "a6ad5cb4022b02709b"; + const char *expected_tag_hex = "eead9d67890cbb22392336fea1851f38"; + + uint8_t key[32], nonce[12], aad[12], expected_tag[16]; + uint8_t plaintext[1024], ciphertext[1024], decrypted[1024], expected_ct[1024]; + uint8_t tag[16]; + size_t pt_len = strlen(plaintext_hex) / 2; + size_t aad_len = strlen(aad_hex) / 2; + size_t ct_len = strlen(expected_ct_hex) / 2; + + printf("=== ChaCha20-Poly1305 RFC 8439 Appendix A.5 ===\n"); + + if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 || + hex_to_bytes(nonce_hex, nonce, sizeof(nonce)) != 0 || + hex_to_bytes(aad_hex, aad, aad_len) != 0 || + hex_to_bytes(plaintext_hex, plaintext, pt_len) != 0 || + hex_to_bytes(expected_ct_hex, expected_ct, ct_len) != 0 || + hex_to_bytes(expected_tag_hex, expected_tag, sizeof(expected_tag)) != 0) { + printf("❌ hex parse failed\n\n"); + return 0; + } + + if (nostr_chacha20poly1305_encrypt(key, nonce, aad, aad_len, + plaintext, pt_len, ciphertext, tag) != 0) { + printf("❌ encrypt failed\n\n"); + return 0; + } + + if (!bytes_equal(ciphertext, expected_ct, ct_len)) { + size_t i; + size_t mismatch_count = 0; + printf("❌ ciphertext mismatch\n"); + for (i = 0; i < ct_len; i++) { + if (ciphertext[i] != expected_ct[i]) { + if (mismatch_count == 0) { + size_t start = (i > 8) ? (i - 8) : 0; + size_t end = (i + 8 < ct_len) ? (i + 8) : (ct_len - 1); + size_t j; + printf("first mismatch at byte %zu: got=%02x expected=%02x\n", i, ciphertext[i], expected_ct[i]); + printf("context (got): "); + for (j = start; j <= end; j++) printf("%02x", ciphertext[j]); + printf("\ncontext (expected): "); + for (j = start; j <= end; j++) printf("%02x", expected_ct[j]); + printf("\n"); + } + mismatch_count++; + } + } + printf("total mismatched bytes: %zu\n\n", mismatch_count); + return 0; + } + + if (!bytes_equal(tag, expected_tag, sizeof(tag))) { + printf("❌ tag mismatch\n\n"); + return 0; + } + + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, aad_len, + ciphertext, ct_len, tag, decrypted) != 0) { + printf("❌ decrypt failed\n\n"); + return 0; + } + + if (!bytes_equal(decrypted, plaintext, pt_len)) { + printf("❌ decrypted plaintext mismatch\n\n"); + return 0; + } + + printf("βœ… passed\n\n"); + return 1; +} + +static int test_aead_tamper_rejects(void) { + uint8_t key[32] = {0}; + uint8_t nonce[12] = {0}; + uint8_t aad[8] = {1,2,3,4,5,6,7,8}; + uint8_t pt[32] = "hello chacha20-poly1305 world"; + uint8_t ct[64] = {0}; + uint8_t tag[16] = {0}; + uint8_t out[64] = {0}; + + printf("=== AEAD tamper rejection ===\n"); + + if (nostr_chacha20poly1305_encrypt(key, nonce, aad, sizeof(aad), pt, strlen((char *)pt), ct, tag) != 0) { + printf("❌ encrypt failed\n\n"); + return 0; + } + + ct[0] ^= 0x01; + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) { + printf("❌ tampered ciphertext accepted\n\n"); + return 0; + } + ct[0] ^= 0x01; + + tag[0] ^= 0x80; + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) { + printf("❌ tampered tag accepted\n\n"); + return 0; + } + tag[0] ^= 0x80; + + aad[0] ^= 0x01; + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) { + printf("❌ tampered AAD accepted\n\n"); + return 0; + } + + printf("βœ… passed\n\n"); + return 1; +} + +int main(void) { + int pass = 1; + + pass &= test_poly1305_rfc8439_2_5_2(); + pass &= test_aead_rfc8439_appendix_a_5(); + pass &= test_aead_tamper_rejects(); + + if (pass) { + printf("πŸŽ‰ All ChaCha20-Poly1305/Poly1305 tests PASSED\n"); + return 0; + } + + printf("πŸ’₯ Some tests FAILED\n"); + return 1; +} diff --git a/tests/crypto_test b/tests/crypto_test index 9c945079..454e94d0 100755 Binary files a/tests/crypto_test and b/tests/crypto_test differ diff --git a/tests/enhanced_header_test b/tests/enhanced_header_test index 9e309dfa..b5c83947 100755 Binary files a/tests/enhanced_header_test and b/tests/enhanced_header_test differ diff --git a/tests/http_test b/tests/http_test index 8ce9ec87..34b801fd 100755 Binary files a/tests/http_test and b/tests/http_test differ diff --git a/tests/nip01_test b/tests/nip01_test index c01b3439..dbeb7424 100755 Binary files a/tests/nip01_test and b/tests/nip01_test differ diff --git a/tests/nip01_test.c b/tests/nip01_test.c index 15fab2e6..a6b215a3 100644 --- a/tests/nip01_test.c +++ b/tests/nip01_test.c @@ -377,7 +377,123 @@ int test_invalid_signature(void) { return (crypto_result == NOSTR_ERROR_EVENT_INVALID_SIGNATURE); } -// Test 10: Integration test with our own event creation +// Test 10: Offline equivalence between legacy and signer-based event creation +int test_event_creation_equivalence_with_signer(void) { + print_test_header("Offline Equivalence - Legacy vs Signer Event Creation"); + + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + unsigned char private_key[32]; + const time_t fixed_ts = 1700000000; + cJSON* legacy_tags = NULL; + cJSON* signer_tags = NULL; + cJSON* tag = NULL; + cJSON* legacy_event = NULL; + cJSON* signer_event = NULL; + nostr_signer_t* signer = NULL; + cJSON* legacy_id_item; + cJSON* signer_id_item; + cJSON* legacy_sig_item; + cJSON* signer_sig_item; + char* legacy_compact = NULL; + char* signer_compact = NULL; + int passed = 0; + + if (nostr_hex_to_bytes(private_key_hex, private_key, 32) != 0) { + printf("❌ Failed to decode private key hex\n"); + return 0; + } + + legacy_tags = cJSON_CreateArray(); + signer_tags = cJSON_CreateArray(); + if (!legacy_tags || !signer_tags) { + printf("❌ Failed to allocate tags arrays\n"); + goto cleanup; + } + + tag = cJSON_CreateArray(); + cJSON_AddItemToArray(tag, cJSON_CreateString("test")); + cJSON_AddItemToArray(tag, cJSON_CreateString("equivalence")); + cJSON_AddItemToArray(legacy_tags, tag); + + tag = cJSON_CreateArray(); + cJSON_AddItemToArray(tag, cJSON_CreateString("test")); + cJSON_AddItemToArray(tag, cJSON_CreateString("equivalence")); + cJSON_AddItemToArray(signer_tags, tag); + + legacy_event = nostr_create_and_sign_event(1, "Equivalence test message", legacy_tags, private_key, fixed_ts); + if (!legacy_event) { + printf("❌ Legacy event creation failed\n"); + goto cleanup; + } + + signer = nostr_signer_local(private_key); + if (!signer) { + printf("❌ Failed to initialize local signer\n"); + goto cleanup; + } + + signer_event = nostr_create_and_sign_event_with_signer(1, "Equivalence test message", signer_tags, signer, fixed_ts); + if (!signer_event) { + printf("❌ Signer-based event creation failed\n"); + goto cleanup; + } + + legacy_id_item = cJSON_GetObjectItem(legacy_event, "id"); + signer_id_item = cJSON_GetObjectItem(signer_event, "id"); + legacy_sig_item = cJSON_GetObjectItem(legacy_event, "sig"); + signer_sig_item = cJSON_GetObjectItem(signer_event, "sig"); + + if (!cJSON_IsString(legacy_id_item) || !cJSON_IsString(signer_id_item) || + !cJSON_IsString(legacy_sig_item) || !cJSON_IsString(signer_sig_item)) { + printf("❌ Missing id/sig fields in one or both events\n"); + goto cleanup; + } + + printf("Legacy ID : %s\n", legacy_id_item->valuestring); + printf("Signer ID : %s\n", signer_id_item->valuestring); + printf("Legacy SIG: %s\n", legacy_sig_item->valuestring); + printf("Signer SIG: %s\n", signer_sig_item->valuestring); + + if (strcmp(legacy_id_item->valuestring, signer_id_item->valuestring) != 0) { + printf("❌ Event IDs differ\n"); + goto cleanup; + } + + if (strcmp(legacy_sig_item->valuestring, signer_sig_item->valuestring) != 0) { + printf("❌ Event signatures differ\n"); + goto cleanup; + } + + legacy_compact = cJSON_PrintUnformatted(legacy_event); + signer_compact = cJSON_PrintUnformatted(signer_event); + if (!legacy_compact || !signer_compact) { + printf("❌ Failed to serialize one or both events for comparison\n"); + goto cleanup; + } + + if (strcmp(legacy_compact, signer_compact) != 0) { + printf("❌ Full event JSON differs\n"); + printf("Legacy: %s\n", legacy_compact); + printf("Signer: %s\n", signer_compact); + goto cleanup; + } + + printf("βœ… Legacy and signer-based creation produced identical event JSON\n"); + passed = 1; + +cleanup: + if (legacy_compact) free(legacy_compact); + if (signer_compact) free(signer_compact); + if (signer_event) cJSON_Delete(signer_event); + if (legacy_event) cJSON_Delete(legacy_event); + if (signer) nostr_signer_free(signer); + if (legacy_tags) cJSON_Delete(legacy_tags); + if (signer_tags) cJSON_Delete(signer_tags); + + return passed; +} + +// Test 11: Integration test with our own event creation int test_integration_with_event_creation(void) { print_test_header("Integration Test - Validate Our Own Created Event"); @@ -472,6 +588,11 @@ int main(void) { print_test_result(test_result, "Invalid Signature - Cryptographic Test"); if (!test_result) all_passed = 0; + // Offline equivalence test + test_result = test_event_creation_equivalence_with_signer(); + print_test_result(test_result, "Offline Equivalence - Legacy vs Signer Event Creation"); + if (!test_result) all_passed = 0; + // Integration test test_result = test_integration_with_event_creation(); print_test_result(test_result, "Integration Test - Validate Our Own Created Event"); diff --git a/tests/nip03_test b/tests/nip03_test index b75b09cb..349722ed 100755 Binary files a/tests/nip03_test and b/tests/nip03_test differ diff --git a/tests/nip04_test b/tests/nip04_test index 748b2526..45928acb 100755 Binary files a/tests/nip04_test and b/tests/nip04_test differ diff --git a/tests/nip05_test b/tests/nip05_test index 2c5a0093..b91eda11 100755 Binary files a/tests/nip05_test and b/tests/nip05_test differ diff --git a/tests/nip11_test b/tests/nip11_test index 088c8c19..3c072d4a 100755 Binary files a/tests/nip11_test and b/tests/nip11_test differ diff --git a/tests/nip13_test b/tests/nip13_test index 00cc85b2..2e1bc24a 100755 Binary files a/tests/nip13_test and b/tests/nip13_test differ diff --git a/tests/nip17_test b/tests/nip17_test index 3a9e80a8..fbf0ea21 100755 Binary files a/tests/nip17_test and b/tests/nip17_test differ diff --git a/tests/nip17_test.c b/tests/nip17_test.c index 5a0bf52d..77c6a9d3 100644 --- a/tests/nip17_test.c +++ b/tests/nip17_test.c @@ -151,7 +151,8 @@ int main(int argc, char* argv[]) { 1, sender_privkey, gift_wraps, - 10 + 10, + 0 ); cJSON_Delete(dm_event); // Original DM event no longer needed diff --git a/tests/nip21_test b/tests/nip21_test index d2e7dc3b..6fe7823c 100755 Binary files a/tests/nip21_test and b/tests/nip21_test differ diff --git a/tests/nip42_pool_test b/tests/nip42_pool_test index 09936484..6face50e 100755 Binary files a/tests/nip42_pool_test and b/tests/nip42_pool_test differ diff --git a/tests/nip42_test b/tests/nip42_test index 2c824afa..b05c6014 100755 Binary files a/tests/nip42_test and b/tests/nip42_test differ diff --git a/tests/nip44_test b/tests/nip44_test index fbcfbd66..3a9ab8ee 100755 Binary files a/tests/nip44_test and b/tests/nip44_test differ diff --git a/tests/nip46_test b/tests/nip46_test index 87468906..0259ef1a 100755 Binary files a/tests/nip46_test and b/tests/nip46_test differ diff --git a/tests/nip60_live_receive_test b/tests/nip60_live_receive_test index 79a272c5..8d2ce639 100755 Binary files a/tests/nip60_live_receive_test and b/tests/nip60_live_receive_test differ diff --git a/tests/nip60_test b/tests/nip60_test index 12d11b27..cdfd5916 100755 Binary files a/tests/nip60_test and b/tests/nip60_test differ diff --git a/tests/nip61_test b/tests/nip61_test index 26772590..8f63803a 100755 Binary files a/tests/nip61_test and b/tests/nip61_test differ diff --git a/tests/nostr_http_test b/tests/nostr_http_test index bb7e5e31..eaa5526d 100755 Binary files a/tests/nostr_http_test and b/tests/nostr_http_test differ diff --git a/tests/nsigner_client_test.c b/tests/nsigner_client_test.c new file mode 100644 index 00000000..4a69aa02 --- /dev/null +++ b/tests/nsigner_client_test.c @@ -0,0 +1,349 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../nostr_core/nostr_common.h" +#include "../nostr_core/nsigner_transport.h" +#include "../nostr_core/nsigner_client.h" +#include "../nostr_core/utils.h" +#include "../cjson/cJSON.h" + +static int test_count = 0; +static int passed_count = 0; + +static void run_result(const char* name, int ok) { + test_count++; + if (ok) { + passed_count++; + printf("βœ… PASS: %s\n", name); + } else { + printf("❌ FAIL: %s\n", name); + } +} + +static int write_full_fd(int fd, const unsigned char* buf, size_t len) { + size_t off = 0; + + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { + return -1; + } + if (n == 0) { + return -1; + } + off += (size_t)n; + } + + return 0; +} + +static int test_framing_round_trip(void) { + int fds[2] = {-1, -1}; + const char* msg = "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[]}"; + char* out = NULL; + size_t out_len = 0; + int ok = 0; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { + return 0; + } + + if (nsigner_transport_send_framed_fd(fds[0], msg, strlen(msg)) != 0) { + goto cleanup; + } + + if (nsigner_transport_recv_framed_fd(fds[1], &out, &out_len) != 0) { + goto cleanup; + } + + if (out_len != strlen(msg)) { + goto cleanup; + } + + if (strcmp(out, msg) != 0) { + goto cleanup; + } + + ok = 1; + +cleanup: + free(out); + if (fds[0] >= 0) close(fds[0]); + if (fds[1] >= 0) close(fds[1]); + return ok; +} + +static int test_framing_fragmented_read_reassembly(void) { + int fds[2] = {-1, -1}; + const char* msg = "{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"abc\"]}"; + unsigned int len = (unsigned int)strlen(msg); + unsigned char hdr[4]; + char* out = NULL; + size_t out_len = 0; + int ok = 0; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { + return 0; + } + + hdr[0] = (unsigned char)((len >> 24) & 0xFFU); + hdr[1] = (unsigned char)((len >> 16) & 0xFFU); + hdr[2] = (unsigned char)((len >> 8) & 0xFFU); + hdr[3] = (unsigned char)(len & 0xFFU); + + if (write_full_fd(fds[0], hdr, 1) != 0) goto cleanup; + if (write_full_fd(fds[0], hdr + 1, 3) != 0) goto cleanup; + + if (write_full_fd(fds[0], (const unsigned char*)msg, 5) != 0) goto cleanup; + if (write_full_fd(fds[0], (const unsigned char*)msg + 5, 2) != 0) goto cleanup; + if (write_full_fd(fds[0], (const unsigned char*)msg + 7, strlen(msg) - 7) != 0) goto cleanup; + + if (nsigner_transport_recv_framed_fd(fds[1], &out, &out_len) != 0) { + goto cleanup; + } + + if (out_len != strlen(msg)) { + goto cleanup; + } + + if (strcmp(out, msg) != 0) { + goto cleanup; + } + + ok = 1; + +cleanup: + free(out); + if (fds[0] >= 0) close(fds[0]); + if (fds[1] >= 0) close(fds[1]); + return ok; +} + +static int test_body_hash_matches_sha256_compact_params(void) { + cJSON* params = NULL; + cJSON* selector = NULL; + char* compact = NULL; + unsigned char hash[32]; + char expected_hex[65] = {0}; + char actual_hex[65] = {0}; + int ok = 0; + + params = cJSON_CreateArray(); + if (!params) goto cleanup; + + cJSON_AddItemToArray(params, cJSON_CreateString("peer_pubkey_hex")); + cJSON_AddItemToArray(params, cJSON_CreateString("hello")); + selector = cJSON_CreateObject(); + if (!selector) goto cleanup; + cJSON_AddStringToObject(selector, "role", "main"); + cJSON_AddItemToArray(params, selector); + selector = NULL; + + compact = cJSON_PrintUnformatted(params); + if (!compact) goto cleanup; + + if (nostr_sha256((const unsigned char*)compact, strlen(compact), hash) != 0) { + goto cleanup; + } + nostr_bytes_to_hex(hash, 32, expected_hex); + + if (nsigner_client_compute_body_hash_hex(params, actual_hex) != NOSTR_SUCCESS) { + goto cleanup; + } + + if (strcmp(expected_hex, actual_hex) != 0) { + goto cleanup; + } + + ok = 1; + +cleanup: + if (selector) cJSON_Delete(selector); + if (params) cJSON_Delete(params); + if (compact) free(compact); + return ok; +} + +static int test_unix_discovery_no_crash(void) { + char names[16][64]; + int count = nsigner_transport_list_unix(names, 16); + if (count < 0) { + return 0; + } + + printf("discovery_count=%d\n", count); + return 1; +} + +static int read_exact_fd(int fd, unsigned char* out, size_t len) { + size_t off = 0; + + while (off < len) { + ssize_t n = read(fd, out + off, len - off); + if (n < 0) { + return -1; + } + if (n == 0) { + return -1; + } + off += (size_t)n; + } + + return 0; +} + +static int test_fds_transport_round_trip_via_client(void) { + int fds[2] = {-1, -1}; + pid_t pid = -1; + int status = 0; + int ok = 0; + nsigner_transport_t* transport = NULL; + nsigner_client_t* client = NULL; + cJSON* params = NULL; + cJSON* result = NULL; + char* result_str; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { + return 0; + } + + pid = fork(); + if (pid < 0) { + goto cleanup; + } + + if (pid == 0) { + unsigned char hdr[4]; + unsigned int req_len = 0; + char* req = NULL; + const char* response = "{\"id\":\"1\",\"result\":\"ok-from-fds\"}"; + unsigned int res_len = (unsigned int)strlen(response); + unsigned char out_hdr[4]; + + close(fds[0]); + + if (read_exact_fd(fds[1], hdr, sizeof(hdr)) != 0) { + _exit(2); + } + + req_len = ((unsigned int)hdr[0] << 24) | + ((unsigned int)hdr[1] << 16) | + ((unsigned int)hdr[2] << 8) | + (unsigned int)hdr[3]; + if (req_len == 0 || req_len > 65536U) { + _exit(3); + } + + req = (char*)malloc((size_t)req_len + 1U); + if (req == NULL) { + _exit(4); + } + + if (read_exact_fd(fds[1], (unsigned char*)req, req_len) != 0) { + free(req); + _exit(5); + } + req[req_len] = '\0'; + + if (strstr(req, "\"method\":\"get_public_key\"") == NULL) { + free(req); + _exit(6); + } + free(req); + + out_hdr[0] = (unsigned char)((res_len >> 24) & 0xFFU); + out_hdr[1] = (unsigned char)((res_len >> 16) & 0xFFU); + out_hdr[2] = (unsigned char)((res_len >> 8) & 0xFFU); + out_hdr[3] = (unsigned char)(res_len & 0xFFU); + + if (write_full_fd(fds[1], out_hdr, sizeof(out_hdr)) != 0) { + _exit(7); + } + if (write_full_fd(fds[1], (const unsigned char*)response, res_len) != 0) { + _exit(8); + } + + close(fds[1]); + _exit(0); + } + + close(fds[1]); + fds[1] = -1; + + transport = nsigner_transport_open_fds(fds[0], fds[0], 3000); + if (transport == NULL) { + goto cleanup; + } + fds[0] = -1; + + client = nsigner_client_new(transport); + if (client == NULL) { + transport = NULL; + goto cleanup; + } + transport = NULL; + + params = cJSON_CreateArray(); + if (params == NULL) { + goto cleanup; + } + + if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) { + params = NULL; + goto cleanup; + } + params = NULL; + + if (!cJSON_IsString(result) || result->valuestring == NULL) { + goto cleanup; + } + + result_str = result->valuestring; + if (strcmp(result_str, "ok-from-fds") != 0) { + goto cleanup; + } + + if (waitpid(pid, &status, 0) < 0) { + goto cleanup; + } + pid = -1; + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + goto cleanup; + } + + ok = 1; + +cleanup: + if (result) cJSON_Delete(result); + if (params) cJSON_Delete(params); + if (client) nsigner_client_free(client); + if (transport) transport->close(transport); + if (fds[0] >= 0) close(fds[0]); + if (fds[1] >= 0) close(fds[1]); + if (pid > 0) { + (void)waitpid(pid, &status, 0); + } + return ok; +} + +int main(void) { +#if !defined(NOSTR_ENABLE_NSIGNER_CLIENT) + printf("nsigner client disabled in this build; skipping\n"); + return 0; +#else + run_result("framing round trip through socketpair", test_framing_round_trip()); + run_result("framing reassembles fragmented reads", test_framing_fragmented_read_reassembly()); + run_result("auth body hash matches sha256(compact params)", test_body_hash_matches_sha256_compact_params()); + run_result("/proc/net/unix discovery parser no-crash", test_unix_discovery_no_crash()); + run_result("fds transport client round-trip over framed fd pair", test_fds_transport_round_trip_via_client()); + + printf("\nTotal: %d Passed: %d Failed: %d\n", test_count, passed_count, test_count - passed_count); + return (test_count == passed_count) ? 0 : 1; +#endif +} diff --git a/tests/relay_synchronous_test b/tests/relay_synchronous_test index 162d6a34..8195c265 100755 Binary files a/tests/relay_synchronous_test and b/tests/relay_synchronous_test differ diff --git a/tests/repeated_sync_query_test b/tests/repeated_sync_query_test index a37e8f41..a58f2d24 100755 Binary files a/tests/repeated_sync_query_test and b/tests/repeated_sync_query_test differ diff --git a/tests/signer_modules_test.c b/tests/signer_modules_test.c new file mode 100644 index 00000000..49f505fd --- /dev/null +++ b/tests/signer_modules_test.c @@ -0,0 +1,206 @@ +#include +#include +#include +#include + +#include "../nostr_core/nostr_core.h" + +#define TEST_ASSERT(cond, msg) do { \ + tests_run++; \ + if (cond) { \ + tests_passed++; \ + printf("βœ… %s\n", msg); \ + } else { \ + printf("❌ %s\n", msg); \ + return 1; \ + } \ +} while (0) + +static int tests_run = 0; +static int tests_passed = 0; + +static cJSON* parse_nostr_auth_header_event(const char* header) { + if (!header) return NULL; + const char* prefix = "Nostr "; + size_t prefix_len = strlen(prefix); + if (strncmp(header, prefix, prefix_len) != 0) return NULL; + + const char* b64 = header + prefix_len; + size_t b64_len = strlen(b64); + size_t dec_cap = (b64_len * 3U) / 4U + 8U; + unsigned char* decoded = (unsigned char*)malloc(dec_cap); + if (!decoded) return NULL; + + size_t dec_len = base64_decode(b64, decoded); + if (dec_len == 0 || dec_len >= dec_cap) { + free(decoded); + return NULL; + } + decoded[dec_len] = '\0'; + + cJSON* evt = cJSON_Parse((const char*)decoded); + free(decoded); + return evt; +} + +static int test_blossom_auth_signer_equivalence(void) { + const char* priv_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + unsigned char priv[32]; + TEST_ASSERT(nostr_hex_to_bytes(priv_hex, priv, 32) == 0, "loaded deterministic blossom private key"); + + char sha[65]; + memset(sha, 'a', 64); + sha[64] = '\0'; + + char* header_raw = blossom_create_auth_header(priv, "upload", sha, 300); + TEST_ASSERT(header_raw != NULL, "raw blossom auth header created"); + + cJSON* raw_evt = parse_nostr_auth_header_event(header_raw); + TEST_ASSERT(raw_evt != NULL, "raw blossom auth header decodes to event"); + + cJSON* created_at = cJSON_GetObjectItem(raw_evt, "created_at"); + TEST_ASSERT(cJSON_IsNumber(created_at), "raw blossom event has created_at"); + + nostr_signer_t* signer = nostr_signer_local(priv); + TEST_ASSERT(signer != NULL, "local signer created for blossom test"); + + time_t ts = (time_t)cJSON_GetNumberValue(created_at); + char* header_signer = blossom_create_auth_header_with_signer(signer, "upload", sha, 300, ts); + TEST_ASSERT(header_signer != NULL, "signer blossom auth header created"); + + TEST_ASSERT(strcmp(header_raw, header_signer) == 0, + "blossom raw and signer headers are byte-identical for fixed timestamp"); + + free(header_signer); + cJSON_Delete(raw_evt); + free(header_raw); + nostr_signer_free(signer); + return 0; +} + +static int test_nip42_auth_signer_equivalence(void) { + const char* priv_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + unsigned char priv[32]; + TEST_ASSERT(nostr_hex_to_bytes(priv_hex, priv, 32) == 0, "loaded deterministic nip42 private key"); + + const char* challenge = "test_challenge_12345678901234567890123456789012"; + const char* relay_url = "wss://relay.example.com"; + time_t ts = 1700000000; + + nostr_signer_t* signer = nostr_signer_local(priv); + TEST_ASSERT(signer != NULL, "local signer created for nip42 test"); + + cJSON* raw_evt = nostr_nip42_create_auth_event(challenge, relay_url, priv, ts); + TEST_ASSERT(raw_evt != NULL, "raw nip42 auth event created"); + + cJSON* signer_evt = nostr_nip42_create_auth_event_with_signer(challenge, relay_url, signer, ts); + TEST_ASSERT(signer_evt != NULL, "signer nip42 auth event created"); + + char* raw_json = cJSON_PrintUnformatted(raw_evt); + char* signer_json = cJSON_PrintUnformatted(signer_evt); + TEST_ASSERT(raw_json != NULL && signer_json != NULL, "nip42 events serialize"); + TEST_ASSERT(strcmp(raw_json, signer_json) == 0, + "nip42 raw and signer events are byte-identical at fixed timestamp"); + + free(signer_json); + free(raw_json); + cJSON_Delete(signer_evt); + cJSON_Delete(raw_evt); + nostr_signer_free(signer); + return 0; +} + +static int test_nip17_send_receive_with_signer(void) { + const char* sender_priv_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + const char* recipient_priv_hex = "a13f7a8be7c1a24df0b4fd0dfdd274001f4b9a7f17dbf7fdf2057ce5f7b72759"; + unsigned char sender_priv[32]; + unsigned char recipient_priv[32]; + TEST_ASSERT(nostr_hex_to_bytes(sender_priv_hex, sender_priv, 32) == 0, "loaded deterministic sender key"); + TEST_ASSERT(nostr_hex_to_bytes(recipient_priv_hex, recipient_priv, 32) == 0, "loaded deterministic recipient key"); + + nostr_signer_t* sender_signer = nostr_signer_local(sender_priv); + nostr_signer_t* recipient_signer = nostr_signer_local(recipient_priv); + TEST_ASSERT(sender_signer != NULL && recipient_signer != NULL, "sender/recipient signers created"); + + char sender_pubkey_hex[65]; + char recipient_pubkey_hex[65]; + TEST_ASSERT(nostr_signer_get_public_key(sender_signer, sender_pubkey_hex) == NOSTR_SUCCESS, + "sender pubkey derived"); + TEST_ASSERT(nostr_signer_get_public_key(recipient_signer, recipient_pubkey_hex) == NOSTR_SUCCESS, + "recipient pubkey derived"); + + const char* recipients[] = {recipient_pubkey_hex}; + const char* message = "hello via signer path"; + + cJSON* dm_event = nostr_nip17_create_chat_event(message, + recipients, + 1, + NULL, + NULL, + NULL, + sender_pubkey_hex); + TEST_ASSERT(dm_event != NULL, "nip17 dm event created"); + + cJSON* gift_wraps[4] = {0}; + int count = nostr_nip17_send_dm_with_signer(dm_event, + recipients, + 1, + sender_signer, + gift_wraps, + 4, + 0); + cJSON_Delete(dm_event); + TEST_ASSERT(count == 2, "nip17 send_dm_with_signer produced recipient + sender gift wraps"); + TEST_ASSERT(gift_wraps[0] != NULL, "nip17 recipient gift wrap exists"); + + cJSON* received = NULL; + for (int i = 0; i < count && i < 4; i++) { + if (!gift_wraps[i]) continue; + received = nostr_nip17_receive_dm_with_signer(gift_wraps[i], recipient_signer); + if (received) break; + } + TEST_ASSERT(received != NULL, "nip17 receive_dm_with_signer unwraps payload"); + + cJSON* kind = cJSON_GetObjectItem(received, "kind"); + cJSON* content = cJSON_GetObjectItem(received, "content"); + cJSON* tags = cJSON_GetObjectItem(received, "tags"); + cJSON* pubkey = cJSON_GetObjectItem(received, "pubkey"); + + TEST_ASSERT(cJSON_IsNumber(kind) && (int)cJSON_GetNumberValue(kind) == 14, + "nip17 received event has chat kind"); + TEST_ASSERT(cJSON_IsString(content) && strcmp(cJSON_GetStringValue(content), message) == 0, + "nip17 received content matches original"); + TEST_ASSERT(cJSON_IsString(pubkey) && strcmp(cJSON_GetStringValue(pubkey), sender_pubkey_hex) == 0, + "nip17 received pubkey matches sender"); + TEST_ASSERT(cJSON_IsArray(tags), "nip17 received tags is array"); + + cJSON_Delete(received); + for (int i = 0; i < count && i < 4; i++) { + cJSON_Delete(gift_wraps[i]); + } + nostr_signer_free(recipient_signer); + nostr_signer_free(sender_signer); + return 0; +} + +int main(void) { + int rc = 0; + + printf("=== signer_modules_test ===\n"); + + if (nostr_init() != NOSTR_SUCCESS) { + printf("❌ Failed to initialize NOSTR library\n"); + return 1; + } + + if (test_blossom_auth_signer_equivalence() != 0) rc = 1; + else if (test_nip42_auth_signer_equivalence() != 0) rc = 1; + else if (test_nip17_send_receive_with_signer() != 0) rc = 1; + + if (rc == 0) { + printf("\nPassed %d/%d assertions\n", tests_passed, tests_run); + } + + nostr_cleanup(); + return rc; +} diff --git a/tests/simple_async_test b/tests/simple_async_test index ef389b31..a98194fa 100755 Binary files a/tests/simple_async_test and b/tests/simple_async_test differ diff --git a/tests/simple_init_test b/tests/simple_init_test index 8429a037..536365c6 100755 Binary files a/tests/simple_init_test and b/tests/simple_init_test differ diff --git a/tests/streaming_sha256_test b/tests/streaming_sha256_test index f6ebec5e..b43eea4e 100755 Binary files a/tests/streaming_sha256_test and b/tests/streaming_sha256_test differ diff --git a/tests/sync_relay_test b/tests/sync_relay_test index 49b58647..9e5f20c8 100755 Binary files a/tests/sync_relay_test and b/tests/sync_relay_test differ diff --git a/tests/websocket_debug b/tests/websocket_debug index f2d980d3..68e4674d 100755 Binary files a/tests/websocket_debug and b/tests/websocket_debug differ