Compare commits

...
71 Commits
Author SHA1 Message Date
laantungir 363b7e9adf Release v0.5.0 2026-03-19 09:13:59 -04:00
laantungir 3186c2ead6 just want to increment 2026-03-19 09:09:59 -04:00
laantungir f07433578a just want to increment 2026-03-19 09:09:46 -04:00
laantungir d264d9f5ed nip60 2026-03-19 09:01:11 -04:00
Your Name 98094cac86 fix relay pool sync query repeat behavior and add regression test 2026-03-16 05:27:23 -04:00
Your Name 79d98587d3 Delegate nostr_core_lib logging via callback and integrate single-file didactyl logging 2026-03-13 13:57:20 -04:00
Your Name 81814e8518 Various changes 2026-03-05 15:18:44 -04:00
Your Name 784cfc09c3 Add relay state transition cause capture in poll loop 2026-03-05 15:18:13 -04:00
Your Name c87f8db6d8 Fix oversized ws frame handling and increase receive buffers 2026-03-05 14:42:14 -04:00
Your Name c871c40219 query_sync: forward non-matching relay messages to pool dispatch 2026-03-05 14:07:11 -04:00
Your Name 2b3113dafc relay_pool: skip reconnect attempts in publish_async 2026-03-05 11:01:21 -04:00
Your Name 5c51ab1f9f Add socket recv/send timeouts after connect to prevent websocket poll hangs on half-broken connections 2026-03-05 10:39:51 -04:00
laantungir df50ca1273 Add NIP-42 auth support to relay pool with verbose auth diagnostics test and docs updates 2026-03-01 08:49:55 -04:00
laantungir 2ce2ecbbfb Implement NIP-46 remote signing API, tests/example, build integration, and refresh README docs 2026-02-28 09:53:35 -04:00
Your Name 57ecbf0c11 Fix SSL_pending busy-wait causing 100% CPU usage
The tls_recv() function was skipping the select() call when SSL_pending()
returned non-zero, causing a tight busy-wait loop. This fix ensures select()
is always called, using a zero timeout when SSL has buffered data to return
immediately, or the full timeout to properly block when waiting for new data.

This prevents the relay client from consuming 100% CPU while maintaining
the WebSocket connection.
2026-02-03 14:16:00 -04:00
laantungir e87d7364ff Rollback of nip44 changes, and added ability to modify timestamps in giftwraps. 2025-10-27 13:16:06 -04:00
Your Name 0b2cca6549 Add configurable timestamp randomization for NIP-59 gift wraps 2025-10-27 12:57:25 -04:00
laantungir fca1a8077d Revert NIP-44 to spec compliance (65535 bytes), keep NIP-04 at 1MB 2025-10-27 12:51:50 -04:00
laantungir 2c128a6295 Upgrade nip4 and nip44 to be able to handle 1MB payloads. 2025-10-24 18:56:30 -04:00
laantungir 24903b8c90 Adding async publish to relay pool 2025-10-09 09:19:11 -04:00
laantungir c6896eebb4 added nip 17 2025-10-04 18:32:28 -04:00
laantungir 4e06d8f723 Implement NIP-21: nostr: URI scheme with full support for note, nprofile, nevent, and naddr URIs including TLV encoding/decoding and comprehensive test suite 2025-10-03 06:10:56 -04:00
laantungir d7a8aa1dc4 nip 17, and 59 2025-10-03 04:25:10 -04:00
laantungir cdeb569a16 Add enhanced subscription functionality with EOSE result modes 2025-10-02 15:00:50 -04:00
laantungir 2224448d12 Getting the relay pool up to speed 2025-10-02 11:51:41 -04:00
laantungir 9bf74e76ce Updated synchronous relay queries to handle nip42. 2025-09-30 12:16:23 -04:00
laantungir d52ac9c106 Merge branch 'master' of ssh://git.laantungir.net:222/laantungir/nostr_core_lib
Working on Ginxsom auth and NIP 42 on a different computer at the same
time. My bad.
2025-09-07 13:07:02 -04:00
laantungir e087a17eb9 Add NIP-42 implementation and local updates
- Added NIP-42 authentication implementation (nip042.c, nip042.h)
- Added NIP-42 test suite (nip42_test.c, nip42_test)
- Updated common core files for NIP-42 support
- Updated build script
- Rebuilt test binaries
2025-09-07 13:04:53 -04:00
Your Name d7e43328bb Add unified request validation system with authentication rules
- Add request_validator.h/c with comprehensive authentication system
- Implement pluggable database backend interface (SQLite default)
- Add priority-based rule evaluation engine with caching
- Support pubkey whitelist/blacklist, hash blacklist, MIME restrictions
- Add new authentication error codes to nostr_common.h
- Include request_validator.h in nostr_core.h
- Update build.sh to compile request_validator.c
- Designed for shared use between ginxsom and c-relay projects
2025-09-07 09:44:38 -04:00
laantungir 0af77dffab nip13 validation added 2025-09-05 13:42:03 -04:00
laantungir 0f72d7433a readme.md 2025-09-03 15:16:08 -04:00
laantungir 2f3140d4f6 remove exposed .h crypto headers 2025-09-02 12:36:52 -04:00
laantungir 31947d12b7 Streaming sha256 2025-08-19 11:24:48 -04:00
laantungir 9e9fcba96c version 2025-08-19 07:02:42 -04:00
laantungir 4a096ff35f Nostr note validation added to nip01 2025-08-19 06:59:04 -04:00
laantungir 64aee10a21 clean up 2025-08-17 14:14:52 -04:00
laantungir a9a1aff682 Fixed bug in nip44.c, was an error in ecdh_shared_secret. Added comments 2025-08-17 11:29:07 -04:00
laantungir a4fbba027e Fixed error in nip04 implementation. Now working 2025-08-17 10:42:38 -04:00
laantungir fdc7cc2a38 comparison test for debugging 2025-08-16 17:48:02 -04:00
laantungir 9879457d7e remove secp256 from project 2025-08-16 14:09:26 -04:00
laantungir 6722d09579 feat: migrate to system dependencies from static linking
BREAKING CHANGE: Library now requires system-installed dependencies

Major Changes:
- Convert secp256k1 from bundled static lib to system dependency
- Convert OpenSSL from bundled static lib to system dependency
- Convert curl from bundled static lib to system dependency
- Update build.sh with pkg-config detection and fallback logic
- Remove all static library extraction/building logic
- Update README.md with new dependency requirements and installation

Build System:
- Add detect_system_secp256k1() with pkg-config support
- Add detect_system_openssl() with pkg-config support
- Add detect_system_curl() with pkg-config support
- Remove secp256k1 building/extraction from ar archive
- Update CFLAGS and LIBS to use system library variables
- Clear error messages for missing dependencies with install commands

Documentation:
- Add system dependency installation for Ubuntu/Debian/CentOS/macOS
- Update all compile/link examples to include -lssl -lcrypto -lcurl -lsecp256k1
- Remove references to 'self-contained' and 'no external dependencies'
- Update integration examples throughout README

Benefits:
- Smaller library size (only internal code bundled)
- Automatic security updates via system package manager
- Standard Linux library distribution pattern
- Reduced build complexity
- Better system integration with pkg-config

Required Installation:
Ubuntu/Debian: sudo apt install libssl-dev libcurl4-openssl-dev libsecp256k1-dev
CentOS/RHEL: sudo yum install openssl-devel libcurl-devel libsecp256k1-devel
macOS: brew install openssl curl secp256k1
2025-08-16 13:59:29 -04:00
laantungir 3533c874ea un-nest curl 2025-08-16 11:22:44 -04:00
laantungir 553774fdc4 secp256k1 2025-08-16 10:48:58 -04:00
laantungir 3f5024c24c Add in secp256k repo so that customer can build. 2025-08-16 10:39:49 -04:00
laantungir 8bcf76387b Updated build.sh to build curl, openssl, and 256k1 if needed 2025-08-16 10:26:39 -04:00
laantungir 7bf39a7e10 Simplify build script directory validation
- Replace file-based validation with directory name check
- Now simply checks if current directory is named 'nostr_core_lib'
- More reliable and clearer error messages for customers
- Fixes issue where customers get validation errors when running from properly named directories
2025-08-16 09:45:27 -04:00
laantungir ec5a97f951 Update library usage documentation 2025-08-16 09:36:39 -04:00
laantungir 9913f92c96 Fix build script path resolution for customer Makefile usage
- Added BUILD_DIR=/home/teknari/Sync/Programming/VibeCoding/nostr_core_lib to capture absolute build directory
- Changed relative paths to absolute paths in archive extraction
- Fixes 'No such file or directory' error when build.sh is called from customer Makefiles
- Maintains backward compatibility for all existing use cases
2025-08-16 09:34:57 -04:00
laantungir dad1f5aa52 Working in the mines 2025-08-16 08:51:04 -04:00
laantungir 241ca036ed Refactor and consolidate test suite
- Moved old tests from tests/old/ to main tests/ directory
- Renamed nostr_test_bip32.c to bip32_test.c for consistency
- Renamed nostr_crypto_test.c to crypto_test.c for consistency
- Renamed wss_test.c and moved from old directory
- Fixed unused variable warning in bip32_test.c
- Updated build system and workspace rules
- Cleaned up old compiled test executables
- Updated nostr_common.h and core_relays.c
- Removed obsolete nostr_core.h.old backup file

This consolidates all active tests into the main directory and removes
outdated test files while maintaining a clean, organized structure.
2025-08-16 08:38:41 -04:00
laantungir 997c3e976b Completed refactoring to separate nip files, and updating build.sh 2025-08-16 07:42:48 -04:00
laantungir ea9b8918f6 Last version before deleting Makefile and CmakeLists.txt 2025-08-15 16:32:59 -04:00
laantungir 52e5d300fc Final fix for curl callback function signature - use proper void* types 2025-08-15 09:14:53 -04:00
laantungir ace0b5a792 Fixed curl type warnings - callback function signature and timeout parameter casting 2025-08-15 09:12:55 -04:00
laantungir 04f22e6f47 Improved POW 2025-08-15 07:47:16 -04:00
laantungir e8c0fcdf63 . 2025-08-14 19:15:55 -04:00
laantungir 7cf7ffe025 . 2025-08-14 18:30:16 -04:00
laantungir a2b700cf40 version 2025-08-14 17:52:45 -04:00
laantungir dfe8a20e2e Add automatic dependency building to Makefile
- Add secp256k1 auto-building rule for x64 target
- Fix customer build errors with missing secp256k1/.libs/libsecp256k1.a
- Force clean rebuild when secp256k1 library is missing
- Customers can now run 'make' directly without manual dependency building
- Maintains existing ARM64 cross-compilation auto-building functionality
2025-08-14 17:43:33 -04:00
laantungir 3cfddbe75b . 2025-08-14 16:43:46 -04:00
laantungir 8c610b6e36 Fix submodule compatibility: Remove version.c dependency and include cjson source files
- Remove nostr_core/version.c from Makefile LIB_SOURCES to fix submodule build failures
- Remove cjson/ from .gitignore and add cjson source files to repository
- Consolidate cJSON.h into cjson/ directory (remove duplicate from nostr_core/)
- Simplify build.sh versioning to only manage git tags and VERSION file
- Update examples/version_test.c to work without version functions
- Consumer projects can now use 'make' directly without missing file errors
2025-08-14 16:40:34 -04:00
laantungir 2de890da5c clean up versioning 2025-08-14 16:26:37 -04:00
laantungir bb673cc1a4 Initial template structure from nostr_core_lib
- Complete C library template with OpenSSL-based crypto
- Comprehensive build system (Makefile, build.sh)
- Example code and test suite
- Documentation and usage guides
- Cross-platform compatibility (x64/ARM64)
- Production-ready structure for C library projects
2025-08-14 15:10:59 -04:00
laantungir f6c8ef6246 Adding in curl and openssl repos 2025-08-14 12:09:30 -04:00
laantungir 01d382c0d3 Complete mbedTLS cleanup - Removed all mbedTLS dependencies and updated documentation
- Moved mbedTLS directories to Trash/ (mbedtls/, mbedtls-install/, mbedtls-arm64-install/)
- Removed obsolete nostr_websocket_mbedtls.c implementation
- Updated nostr_websocket/Makefile to use OpenSSL instead of mbedTLS
- Updated nostr_websocket/README.md with OpenSSL migration notes
- Updated nostr_websocket/EXPORT_GUIDE.md with OpenSSL instructions
- All tests pass: crypto tests, static linking verification
- Build system fully functional with OpenSSL-only dependencies
2025-08-14 11:59:03 -04:00
laantungir f86fb256b2 Pre-mbedTLS cleanup checkpoint - OpenSSL migration complete, all tests passing 2025-08-14 11:51:26 -04:00
laantungir ac85ab6f07 Last commit before switching from mbedtls to openssl 2025-08-14 09:58:24 -04:00
laantungir cfde3bcb0f cJSON.h added 2025-08-12 21:05:39 -04:00
laantungir a4ae8df66f Fully statically linked for both x64 and arm64. Updated build.sh to always compile both versions 2025-08-11 06:54:50 -04:00
laantungir 3108661b0a added nostr_decode_npub 2025-08-09 11:40:34 -04:00
laantungir 3b076ea372 . 2025-08-09 11:07:11 -04:00
146 changed files with 29957 additions and 10156 deletions
+12
View File
@@ -0,0 +1,12 @@
This library is fully staticly linked. There should be no external dependencies.
When building, use build.sh, not make. When building for tests use build.sh -t
When making TUI menus, try to use the first leter of the command and the key to press to execute that command. For example, if the command is "Open file" try to use a keypress of "o" upper or lower case to signal to open the file. Use this instead of number keyed menus when possible. In the command, the letter should be underlined that signifies the command.
When deleting, everything gets moved to the Trash folder.
MAKEFILE POLICY: There should be only ONE Makefile in the entire project. All build logic (library, tests, examples, websocket) must be consolidated into the root Makefile. Do not create separate Makefiles in subdirectories as this creates testing inconsistencies where you test with one Makefile but run with another.
TESTS POLICY: On the printout, dont just show pass or fail, show the expected values, and what the actual result was. If dealing with nostr events, print the entire json event. If dealing with relay communication, show the whole communication.
+3 -7
View File
@@ -1,5 +1,4 @@
Trash/
cjson/
cline_history/
libsodium/
monocypher-4.0.2/
@@ -8,10 +7,9 @@ nips/
node_modules/
nostr-tools/
tiny-AES-c/
mbedtls/
mbedtls-arm64-install/
mbedtls-install/
secp256k1/
blossom/
ndk/
Trash/debug_tests/
node_modules/
@@ -21,5 +19,3 @@ node_modules/
*.dylib
*.dll
build/
mbedtls-install/
mbedtls-arm64-install/
+7
View File
@@ -0,0 +1,7 @@
---
description: "Increments and pushes the repo"
---
Run increment_and_push.sh followed in the command line with a good description of the changes that were made.
For example: ./increment_and_push.sh "Fixed that nasty bug"
+3
View File
@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}
-88
View File
@@ -1,88 +0,0 @@
# NOSTR Core Library - Cleanup Report
## Overview
After successfully resolving the NIP-04 ECDH compatibility issues, we performed a comprehensive cleanup of debugging artifacts and temporary files created during the troubleshooting process.
## Files Moved to Trash/debug_tests/
### NIP-04 Debug Tests (Created During Troubleshooting)
- `aes_debug_test.c/.exe` - AES encryption debugging
- `ecdh_debug_test.c/.exe` - ECDH shared secret debugging
- `ecdh_x_coordinate_test.c/.exe` - X coordinate extraction testing
- `ecdh_comprehensive_debug_test.c/.exe` - Comprehensive ECDH testing
- `nip04_decrypt_debug_test.c/.exe` - Decryption specific debugging
- `nip04_detailed_debug_test.c/.exe` - Detailed step-by-step debugging
- `nip04_ecdh_debug_test.c/.exe` - NIP-04 ECDH specific testing
- `nip04_encrypt_only_test.c/.exe` - Encryption-only testing
- `nip04_minimal_test.c/.exe` - Minimal test cases
- `nip04_simple_test.c/.exe` - Simple test implementation
- `nip04_step_by_step_debug_test.c/.exe` - Step-by-step debugging
- `decrypt_debug_minimal.c/.exe` - Minimal decryption debugging
- `noble_vs_libsecp_comparison.c/.exe` - JavaScript comparison testing
### Other Debug Files
- `debug_bip32.c/.exe` - BIP32 debugging
- `debug_bip32_test.c/.exe` - BIP32 test debugging
- `frame_debug_test.c/.exe` - Frame debugging
- `debug.log` - **9.8GB debug log file** (major space savings!)
### JavaScript Reference Implementation
- `nostr-tools/` - JavaScript reference implementation used for comparison
- `nip04.ts` - TypeScript NIP-04 implementation
- `debug_nip04.js` - JavaScript debugging script
## Files Kept (Essential Tests)
### Core Functionality Tests
- `nip04_test.c` - **Main comprehensive NIP-04 test** (our final working test)
- `simple_init_test.c` - Basic library initialization test
- `nostr_crypto_test.c` - Cryptographic functions test
- `nostr_test_bip32.c` - BIP32 HD wallet test
- `relay_pool_test.c` - Relay pool functionality test
- `sync_test.c` - Synchronization test
- `test_pow_loop.c` - Proof of work test
### Build Infrastructure
- `Makefile` - Test compilation rules
- `build.tests.sh` - Test build script
## Key Improvements Made
### 1. Function Naming Clarity
- Added `nostr_schnorr_sign()` - clearly indicates BIP-340 Schnorr signatures
- Maintained `nostr_ec_sign()` as legacy wrapper for backward compatibility
- **Benefit**: Prevents future confusion between ECDH and signature operations
### 2. ECDH Compatibility Fix
- Fixed ECDH implementation to match NIP-04 specification exactly
- Custom hash function that extracts only X coordinate (no hashing)
- **Result**: 100% compatible with JavaScript NOSTR ecosystem
### 3. Memory Management
- Fixed buffer overflow issues in NIP-04 decryption
- Proper base64 buffer size calculations
- Enhanced error handling and cleanup
- **Result**: No more segmentation faults
## Final Test Status
```
✅ nip04_test: PASS (Round-trip + Reference compatibility)
✅ Memory management: Fixed (No segfaults)
✅ ECDH compatibility: 100% JavaScript ecosystem compatible
✅ Function naming: Clear and unambiguous
```
## Space Savings
- **Removed 9.8GB debug.log file**
- Cleaned up 20+ debugging test files and executables
- Organized debugging artifacts in Trash/debug_tests/ for easy reference
## Secp256k1 Status
- Checked for extra debugging code: **CLEAN**
- All files are standard libsecp256k1 build artifacts
- No cleanup needed
---
**The NOSTR core library is now in a clean, production-ready state with fully functional NIP-04 encryption/decryption that's compatible with the broader NOSTR ecosystem!**
-187
View File
@@ -1,187 +0,0 @@
cmake_minimum_required(VERSION 3.12)
project(nostr_core VERSION 1.0.0 LANGUAGES C)
# Set C standard
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Build options
option(NOSTR_BUILD_STATIC "Build static library" ON)
option(NOSTR_BUILD_SHARED "Build shared library" ON)
option(NOSTR_BUILD_EXAMPLES "Build examples" ON)
option(NOSTR_BUILD_TESTS "Build tests" ON)
option(NOSTR_ENABLE_WEBSOCKETS "Enable WebSocket support" ON)
option(NOSTR_USE_MBEDTLS "Use mbedTLS for crypto (otherwise use built-in)" OFF)
# Compiler flags
if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID MATCHES "Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror")
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
endif()
# Include directories
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/cjson)
# Source files
set(NOSTR_CORE_SOURCES
nostr_core/core.c
nostr_core/core_relays.c
nostr_core/core_relay_pool.c
nostr_core/nostr_crypto.c
nostr_core/nostr_secp256k1.c
cjson/cJSON.c
)
set(NOSTR_CORE_HEADERS
nostr_core.h
nostr_crypto.h
cjson/cJSON.h
)
# Add mbedTLS if enabled
if(NOSTR_USE_MBEDTLS)
add_subdirectory(mbedtls)
list(APPEND NOSTR_CORE_SOURCES mbedtls_wrapper.c)
add_definitions(-DNOSTR_USE_MBEDTLS=1)
endif()
# Add WebSocket support if enabled
if(NOSTR_ENABLE_WEBSOCKETS)
file(GLOB WEBSOCKET_SOURCES "nostr_websocket/*.c")
file(GLOB WEBSOCKET_HEADERS "nostr_websocket/*.h")
list(APPEND NOSTR_CORE_SOURCES ${WEBSOCKET_SOURCES})
list(APPEND NOSTR_CORE_HEADERS ${WEBSOCKET_HEADERS})
add_definitions(-DNOSTR_ENABLE_WEBSOCKETS=1)
endif()
# Create static library
if(NOSTR_BUILD_STATIC)
add_library(nostr_core_static STATIC ${NOSTR_CORE_SOURCES})
set_target_properties(nostr_core_static PROPERTIES OUTPUT_NAME nostr_core)
target_link_libraries(nostr_core_static m)
if(NOSTR_USE_MBEDTLS)
target_link_libraries(nostr_core_static mbedcrypto mbedx509 mbedtls)
endif()
endif()
# Create shared library
if(NOSTR_BUILD_SHARED)
add_library(nostr_core_shared SHARED ${NOSTR_CORE_SOURCES})
set_target_properties(nostr_core_shared PROPERTIES OUTPUT_NAME nostr_core)
target_link_libraries(nostr_core_shared m)
if(NOSTR_USE_MBEDTLS)
target_link_libraries(nostr_core_shared mbedcrypto mbedx509 mbedtls)
endif()
# Set version information
set_target_properties(nostr_core_shared PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1
)
endif()
# Create alias targets for easier integration
if(NOSTR_BUILD_STATIC)
add_library(nostr_core::static ALIAS nostr_core_static)
endif()
if(NOSTR_BUILD_SHARED)
add_library(nostr_core::shared ALIAS nostr_core_shared)
endif()
# Examples
if(NOSTR_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# Tests
if(NOSTR_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# Installation
include(GNUInstallDirs)
# Install libraries
if(NOSTR_BUILD_STATIC)
install(TARGETS nostr_core_static
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
endif()
if(NOSTR_BUILD_SHARED)
install(TARGETS nostr_core_shared
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif()
# Install headers
install(FILES ${NOSTR_CORE_HEADERS}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/nostr
)
# Install pkg-config file
configure_file(nostr_core.pc.in nostr_core.pc @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/nostr_core.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
)
# Generate export configuration
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/nostr_core-config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/nostr_core-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nostr_core
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/nostr_core-config-version.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/nostr_core-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/nostr_core-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nostr_core
)
# Export targets
if(NOSTR_BUILD_STATIC OR NOSTR_BUILD_SHARED)
set(EXPORT_TARGETS "")
if(NOSTR_BUILD_STATIC)
list(APPEND EXPORT_TARGETS nostr_core_static)
endif()
if(NOSTR_BUILD_SHARED)
list(APPEND EXPORT_TARGETS nostr_core_shared)
endif()
install(EXPORT nostr_core-targets
FILE nostr_core-targets.cmake
NAMESPACE nostr_core::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nostr_core
)
export(TARGETS ${EXPORT_TARGETS}
FILE ${CMAKE_CURRENT_BINARY_DIR}/nostr_core-targets.cmake
NAMESPACE nostr_core::
)
endif()
# Summary
message(STATUS "NOSTR Core Library Configuration:")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " Build static library: ${NOSTR_BUILD_STATIC}")
message(STATUS " Build shared library: ${NOSTR_BUILD_SHARED}")
message(STATUS " Build examples: ${NOSTR_BUILD_EXAMPLES}")
message(STATUS " Build tests: ${NOSTR_BUILD_TESTS}")
message(STATUS " Enable WebSockets: ${NOSTR_ENABLE_WEBSOCKETS}")
message(STATUS " Use mbedTLS: ${NOSTR_USE_MBEDTLS}")
message(STATUS " Install prefix: ${CMAKE_INSTALL_PREFIX}")
-309
View File
@@ -1,309 +0,0 @@
# Exportable C NOSTR Library Design Guide
This document outlines the design principles and structure for making the C NOSTR library easily exportable and reusable across multiple projects.
## Overview
The C NOSTR library is designed as a modular, self-contained implementation that can be easily integrated into other projects while maintaining compatibility with the broader NOSTR ecosystem.
## Design Principles
### 1. Modular Architecture
- **Core Layer**: Essential NOSTR functionality (`nostr_core.c/h`)
- **Crypto Layer**: Self-contained cryptographic primitives (`nostr_crypto.c/h`)
- **WebSocket Layer**: Optional networking functionality (`nostr_websocket/`)
- **Dependencies**: Minimal external dependencies (only cJSON and mbedTLS)
### 2. Clean API Surface
- Consistent function naming with `nostr_` prefix
- Clear return codes using `NOSTR_SUCCESS`/`NOSTR_ERROR_*` constants
- Well-documented function signatures
- No global state where possible
### 3. Self-Contained Crypto
- Custom implementations of SHA-256, HMAC, secp256k1
- BIP39 wordlist embedded in code
- No external crypto library dependencies for core functionality
- Optional mbedTLS integration for enhanced security
### 4. Cross-Platform Compatibility
- Standard C99 code
- Platform-specific code isolated in separate modules
- ARM64 and x86_64 tested builds
- Static library compilation support
## Library Structure
```
c_nostr/
├── nostr_core.h # High-level API
├── nostr_core.c # Implementation
├── nostr_crypto.h # Crypto primitives API
├── nostr_crypto.c # Self-contained crypto
├── libnostr_core.a # Static library (x86_64)
├── libnostr_core.so # Shared library (x86_64)
├── cjson/ # JSON parsing (vendored)
├── mbedtls/ # Optional crypto backend
├── examples/ # Usage examples
├── tests/ # Test suites
└── nostr_websocket/ # Optional WebSocket layer
```
## Integration Methods
### Method 1: Static Library Linking
**Best for**: Applications that want a single binary with all NOSTR functionality embedded.
```bash
# Build the library
make lib
# In your project Makefile:
CFLAGS += -I/path/to/c_nostr
LDFLAGS += -L/path/to/c_nostr -lnostr_core -lm -static
```
**Usage:**
```c
#include "nostr_core.h"
int main() {
if (nostr_init() != NOSTR_SUCCESS) {
return 1;
}
unsigned char private_key[32], public_key[32];
nostr_generate_keypair(private_key, public_key);
cJSON* event = nostr_create_text_event("Hello NOSTR!", private_key);
// ... use event
nostr_cleanup();
return 0;
}
```
### Method 2: Source Code Integration
**Best for**: Applications that want to customize the crypto backend or minimize dependencies.
```bash
# Copy essential files to your project:
cp nostr_core.{c,h} your_project/src/
cp nostr_crypto.{c,h} your_project/src/
cp -r cjson/ your_project/src/
```
**Compile with your project:**
```c
// In your source
#include "nostr_core.h"
// Use NOSTR functions directly
```
### Method 3: Git Submodule
**Best for**: Projects that want to track upstream changes and contribute back.
```bash
# In your project root:
git submodule add https://github.com/yourorg/c_nostr.git deps/c_nostr
git submodule update --init --recursive
# In your Makefile:
CFLAGS += -Ideps/c_nostr
LDFLAGS += -Ldeps/c_nostr -lnostr_core
```
### Method 4: Shared Library
**Best for**: System-wide installation or multiple applications sharing the same NOSTR implementation.
```bash
# Install system-wide
sudo make install
# In your project:
CFLAGS += $(pkg-config --cflags nostr_core)
LDFLAGS += $(pkg-config --libs nostr_core)
```
## API Design for Exportability
### Consistent Error Handling
```c
typedef enum {
NOSTR_SUCCESS = 0,
NOSTR_ERROR_INVALID_INPUT = -1,
NOSTR_ERROR_CRYPTO_FAILED = -2,
NOSTR_ERROR_MEMORY_ALLOCATION = -3,
NOSTR_ERROR_JSON_PARSE = -4,
NOSTR_ERROR_NETWORK = -5
} nostr_error_t;
const char* nostr_strerror(int error_code);
```
### Clean Resource Management
```c
// Always provide cleanup functions
int nostr_init(void);
void nostr_cleanup(void);
// JSON objects returned should be freed by caller
cJSON* nostr_create_text_event(const char* content, const unsigned char* private_key);
// Caller must call cJSON_Delete(event) when done
```
### Optional Features
```c
// Compile-time feature toggles
#ifndef NOSTR_DISABLE_WEBSOCKETS
int nostr_connect_relay(const char* url);
#endif
#ifndef NOSTR_DISABLE_IDENTITY_PERSISTENCE
int nostr_save_identity(const unsigned char* private_key, const char* password, int account);
#endif
```
## Configuration System
### Build-Time Configuration
```c
// nostr_config.h (generated during build)
#define NOSTR_VERSION "1.0.0"
#define NOSTR_HAS_MBEDTLS 1
#define NOSTR_HAS_WEBSOCKETS 1
#define NOSTR_STATIC_BUILD 1
```
### Runtime Configuration
```c
typedef struct {
int log_level;
char* identity_file_path;
int default_account;
int enable_networking;
} nostr_config_t;
int nostr_set_config(const nostr_config_t* config);
const nostr_config_t* nostr_get_config(void);
```
## Testing and Validation
### Ecosystem Compatibility Testing
The library includes comprehensive test suites that validate compatibility with reference implementations like `nak`:
```bash
# Run all tests
cd tests && make test
# Test specific functionality
make test-crypto # Cryptographic primitives
make test-core # High-level NOSTR functions
```
### Test Vectors
Real-world test vectors are generated using `nak` to ensure ecosystem compatibility:
- Key generation and derivation (BIP39/BIP32)
- Event creation and signing
- Bech32 encoding/decoding
- Message serialization
## Documentation for Exporters
### Essential Files Checklist
For projects integrating this library, you need:
**Core Files (Required):**
- `nostr_core.h` - Main API
- `nostr_core.c` - Implementation
- `nostr_crypto.h` - Crypto API
- `nostr_crypto.c` - Self-contained crypto
- `cjson/` directory - JSON parsing
**Optional Files:**
- `nostr_websocket/` - WebSocket relay support
- `mbedtls/` - Enhanced crypto backend
- `examples/` - Usage examples
- `tests/` - Validation tests
### Minimal Integration Example
```c
// minimal_nostr.c - Smallest possible integration
#include "nostr_core.h"
int main() {
// Initialize library
nostr_init();
// Generate keypair
unsigned char priv[32], pub[32];
nostr_generate_keypair(priv, pub);
// Create and sign event
cJSON* event = nostr_create_text_event("Hello from my app!", priv);
char* json_string = cJSON_Print(event);
printf("Event: %s\n", json_string);
// Cleanup
free(json_string);
cJSON_Delete(event);
nostr_cleanup();
return 0;
}
```
**Compile:**
```bash
gcc -I. minimal_nostr.c nostr_core.c nostr_crypto.c cjson/cJSON.c -lm -o minimal_nostr
```
## Future Considerations
### Language Bindings
The C library is designed to be easily wrapped:
- **Python**: Use ctypes or cffi
- **JavaScript**: Use Node.js FFI or WASM compilation
- **Go**: Use cgo
- **Rust**: Use bindgen for FFI bindings
### WebAssembly Support
The library can be compiled to WebAssembly for browser usage:
```bash
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS='["_nostr_init", "_nostr_generate_keypair"]' \
nostr_core.c nostr_crypto.c cjson/cJSON.c -o nostr.wasm
```
### Package Manager Support
Future versions may include:
- pkgconfig files for system installation
- CMake integration for easier builds
- vcpkg/Conan package definitions
## Contributing Back
Projects using this library are encouraged to:
1. Report compatibility issues
2. Submit test vectors from their use cases
3. Contribute performance improvements
4. Add support for additional NIPs
## Version Compatibility
The library follows semantic versioning:
- **Major**: Breaking API changes
- **Minor**: New features, backward compatible
- **Patch**: Bug fixes
API stability guarantees:
- All functions prefixed with `nostr_` are part of the stable API
- Internal functions (static or prefixed with `_`) may change
- Configuration structures may be extended but not modified
---
This design ensures the C NOSTR library can be easily adopted by other projects while maintaining high compatibility with the NOSTR ecosystem standards.
-29
View File
@@ -1,29 +0,0 @@
# c-nostr Export and Implementation Guide
## Overview
This guide provides essential information for developers using the `c-nostr` library, particularly regarding the capabilities and limitations of its self-contained modules, such as the HTTP client.
## HTTP Client (`http_client.c`)
The `http_client` module is a lightweight, self-contained HTTP/HTTPS client designed for simple and direct web requests. It is ideal for tasks like fetching NIP-11 information from Nostr relays or interacting with standard, permissive web APIs.
### Key Features
* **Self-Contained**: It is built with `mbedTLS` and has no external dependencies beyond standard C libraries, making it highly portable.
* **Simplicity**: It provides a straightforward `http_get()` function for making web requests.
* **TLS Support**: It supports HTTPS and basic TLS 1.3/1.2 features, including SNI (Server Name Indication) and ALPN (Application-Layer Protocol Negotiation).
### Limitations
The `http_client` is intentionally simple and is **not a full-featured web browser**. Due to its minimalist design, it will likely be blocked by websites that employ advanced anti-bot protection systems.
* **Incompatibility with Advanced Bot Protection**: Sites like Google, Cloudflare, and others use sophisticated fingerprinting techniques to distinguish between real browsers and automated clients. They analyze the exact combination of TLS cipher suites, TLS extensions, and HTTP headers. Our client's fingerprint does not match a standard browser, so these sites will preemptively drop the connection, typically resulting in a `HTTP_ERROR_NETWORK` error.
* **Intended Use Case**: This client is best suited for interacting with known, friendly APIs and Nostr relays. It is **not** designed for general web scraping or for accessing services that are heavily guarded against automated traffic.
### Best Practices
* **Use for APIs and Relays**: Rely on this client for fetching data from well-defined, public endpoints that do not have aggressive bot-detection measures in place.
* **Avoid Protected Sites**: Do not attempt to use this client to access services like Google Search, as such attempts will fail. For those use cases, a full-featured library like `libcurl` or a dedicated web-scraping framework is required.
* **Check the Test Suite**: The `tests/http_client_test.c` file contains test cases that demonstrate both the successful use cases (e.g., `httpbin.org`) and the expected failures (e.g., `google.com`), providing a clear reference for the client's capabilities.
-266
View File
@@ -1,266 +0,0 @@
# NOSTR Core Library - Usage Guide
## Overview
The NOSTR Core Library (`libnostr_core`) is a self-contained, exportable C library for NOSTR protocol implementation. It requires **no external cryptographic dependencies** (no OpenSSL, no libwally) and can be easily integrated into other projects.
## Key Features
- **Self-Contained Crypto**: All cryptographic operations implemented from scratch
- **Zero External Dependencies**: Only requires standard C library, cJSON, and libwebsockets
- **Cross-Platform**: Builds on Linux, macOS, Windows (with appropriate toolchain)
- **NIP-06 Compliant**: Proper BIP39/BIP32 implementation for key derivation
- **Thread-Safe**: Core cryptographic functions are stateless and thread-safe
- **Easy Integration**: Simple C API with clear error handling
## File Structure for Export
### Core Library Files (Required)
```
libnostr_core/
├── nostr_core.h # Main public API header
├── nostr_core.c # Core implementation
├── nostr_crypto.h # Crypto implementation header
├── nostr_crypto.c # Self-contained crypto implementation
├── Makefile # Build configuration
└── cjson/ # JSON library (can be replaced with system cJSON)
├── cJSON.h
└── cJSON.c
```
### Optional Files
```
├── examples/ # Usage examples (helpful for integration)
├── LIBRARY_USAGE.md # This usage guide
├── SELF_CONTAINED_CRYPTO.md # Crypto implementation details
└── CROSS_PLATFORM_GUIDE.md # Platform-specific build notes
```
## Integration Methods
### Method 1: Static Library Integration
1. **Copy Required Files**:
```bash
cp nostr_core.h nostr_core.c nostr_crypto.h nostr_crypto.c /path/to/your/project/
cp -r cjson/ /path/to/your/project/
```
2. **Build Static Library**:
```bash
gcc -c -fPIC nostr_core.c nostr_crypto.c cjson/cJSON.c
ar rcs libnostr_core.a nostr_core.o nostr_crypto.o cJSON.o
```
3. **Link in Your Project**:
```bash
gcc your_project.c -L. -lnostr_core -lm -o your_project
```
### Method 2: Direct Source Integration
Simply include the source files directly in your project:
```c
// In your project
#include "nostr_core.h"
// Compile with:
// gcc your_project.c nostr_core.c nostr_crypto.c cjson/cJSON.c -lm
```
### Method 3: Shared Library Integration
1. **Build Shared Library**:
```bash
make libnostr_core.so
```
2. **Install System-Wide** (optional):
```bash
sudo cp libnostr_core.so /usr/local/lib/
sudo cp nostr_core.h /usr/local/include/
sudo ldconfig
```
3. **Use in Projects**:
```bash
gcc your_project.c -lnostr_core -lm
```
## Basic Usage Example
```c
#include "nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
int main() {
// Initialize the library
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize NOSTR library\n");
return 1;
}
// Generate a keypair
unsigned char private_key[32];
unsigned char public_key[32];
if (nostr_generate_keypair(private_key, public_key) != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to generate keypair\n");
nostr_cleanup();
return 1;
}
// Convert to hex and bech32
char private_hex[65], public_hex[65];
char nsec[100], npub[100];
nostr_bytes_to_hex(private_key, 32, private_hex);
nostr_bytes_to_hex(public_key, 32, public_hex);
nostr_key_to_bech32(private_key, "nsec", nsec);
nostr_key_to_bech32(public_key, "npub", npub);
printf("Private Key: %s\n", private_hex);
printf("Public Key: %s\n", public_hex);
printf("nsec: %s\n", nsec);
printf("npub: %s\n", npub);
// Create and sign an event
cJSON* event = nostr_create_text_event("Hello NOSTR!", private_key);
if (event) {
char* event_json = cJSON_Print(event);
printf("Signed Event: %s\n", event_json);
free(event_json);
cJSON_Delete(event);
}
// Cleanup
nostr_cleanup();
return 0;
}
```
## Dependency Management
### Required Dependencies
- **Standard C Library**: malloc, string functions, file I/O
- **Math Library**: `-lm` (for cryptographic calculations)
- **cJSON**: JSON parsing (included, or use system version)
### Optional Dependencies
- **libwebsockets**: Only needed for relay communication functions
- **System cJSON**: Can replace bundled version
### Minimal Integration (Crypto Only)
If you only need key generation and signing:
```bash
# Build with minimal dependencies
gcc -DNOSTR_CRYPTO_ONLY your_project.c nostr_crypto.c -lm
```
## Cross-Platform Considerations
### Linux
- Works out of the box with GCC
- Install build-essential: `sudo apt install build-essential`
### macOS
- Works with Xcode command line tools
- May need: `xcode-select --install`
### Windows
- Use MinGW-w64 or MSYS2
- Or integrate with Visual Studio project
### Embedded Systems
- Library is designed to work on resource-constrained systems
- No heap allocations in core crypto functions
- Stack usage is predictable and bounded
## API Reference
### Initialization
```c
int nostr_init(void); // Initialize library
void nostr_cleanup(void); // Cleanup resources
const char* nostr_strerror(int error); // Get error string
```
### Key Generation
```c
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key);
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key);
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key);
```
### Event Creation
```c
cJSON* nostr_create_text_event(const char* content, const unsigned char* private_key);
cJSON* nostr_create_profile_event(const char* name, const char* about,
const unsigned char* private_key);
int nostr_sign_event(cJSON* event, const unsigned char* private_key);
```
### Utilities
```c
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex);
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len);
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output);
nostr_input_type_t nostr_detect_input_type(const char* input);
```
## Error Handling
All functions return standardized error codes:
- `NOSTR_SUCCESS` (0): Operation successful
- `NOSTR_ERROR_INVALID_INPUT` (-1): Invalid parameters
- `NOSTR_ERROR_CRYPTO_FAILED` (-2): Cryptographic operation failed
- `NOSTR_ERROR_MEMORY_FAILED` (-3): Memory allocation failed
- `NOSTR_ERROR_IO_FAILED` (-4): File I/O operation failed
- `NOSTR_ERROR_NETWORK_FAILED` (-5): Network operation failed
## Security Considerations
1. **Private Key Handling**: Always clear private keys from memory when done
2. **Random Number Generation**: Uses `/dev/urandom` on Unix systems
3. **Memory Safety**: All buffers are bounds-checked
4. **Constant-Time Operations**: Critical crypto operations are timing-attack resistant
## Testing Your Integration
Use the provided examples to verify your integration:
```bash
# Test key generation
./examples/simple_keygen
# Test mnemonic functionality
./examples/mnemonic_generation
# Test event creation
./examples/text_event
# Test all functionality
./examples/utility_functions
```
## Support and Documentation
- See `examples/` directory for comprehensive usage examples
- Check `SELF_CONTAINED_CRYPTO.md` for cryptographic implementation details
- Review `CROSS_PLATFORM_GUIDE.md` for platform-specific notes
- All functions are documented in `nostr_core.h`
## License
This library is designed to be freely integrable into other projects. Check the individual file headers for specific licensing information.
---
**Quick Start**: Copy `nostr_core.h`, `nostr_core.c`, `nostr_crypto.h`, `nostr_crypto.c`, and the `cjson/` folder to your project, then compile with `-lm`. That's it!
-142
View File
@@ -1,142 +0,0 @@
# NOSTR Core Library Makefile
# Standalone library build system
CC = gcc
AR = ar
CFLAGS = -Wall -Wextra -std=c99 -fPIC -O2
DEBUG_CFLAGS = -Wall -Wextra -std=c99 -fPIC -g -DDEBUG
STATIC_CFLAGS = -Wall -Wextra -std=c99 -O2 -static
# Logging compile flags
LOGGING_FLAGS ?= -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING
ifneq ($(ENABLE_LOGGING),)
LOGGING_FLAGS += -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING
endif
# Include paths
INCLUDES = -I. -Inostr_core -Icjson -Isecp256k1/include
# Library source files
LIB_SOURCES = nostr_core/core.c nostr_core/core_relays.c nostr_core/nostr_crypto.c nostr_core/nostr_secp256k1.c nostr_core/nostr_aes.c nostr_core/nostr_chacha20.c cjson/cJSON.c
LIB_OBJECTS = $(LIB_SOURCES:.c=.o)
ARM64_LIB_OBJECTS = $(LIB_SOURCES:.c=.arm64.o)
# Library outputs (static only)
STATIC_LIB = libnostr_core.a
ARM64_STATIC_LIB = libnostr_core_arm64.a
# Example files
EXAMPLE_SOURCES = $(wildcard examples/*.c)
EXAMPLE_TARGETS = $(EXAMPLE_SOURCES:.c=)
# Default target - build static library
default: $(STATIC_LIB)
# Build all targets (static only)
all: $(STATIC_LIB) examples
# Static library
$(STATIC_LIB): $(LIB_OBJECTS)
@echo "Creating static library: $@"
$(AR) rcs $@ $^
# ARM64 cross-compilation settings
ARM64_CC = aarch64-linux-gnu-gcc
ARM64_AR = aarch64-linux-gnu-ar
ARM64_INCLUDES = -I. -Inostr_core -Icjson
# ARM64 static library
$(ARM64_STATIC_LIB): $(ARM64_LIB_OBJECTS)
@echo "Creating ARM64 static library: $@"
$(ARM64_AR) rcs $@ $^
# Object files (x86_64)
%.o: %.c
@echo "Compiling: $<"
$(CC) $(CFLAGS) $(LOGGING_FLAGS) $(INCLUDES) -c $< -o $@
# ARM64 object files
%.arm64.o: %.c
@echo "Compiling for ARM64: $<"
$(ARM64_CC) $(CFLAGS) $(LOGGING_FLAGS) $(ARM64_INCLUDES) -c $< -o $@
# Examples
examples: $(EXAMPLE_TARGETS)
examples/%: examples/%.c $(STATIC_LIB)
@echo "Building example: $@"
$(CC) $(STATIC_CFLAGS) $(LOGGING_FLAGS) $(INCLUDES) $< -o $@ ./libnostr_core.a ./secp256k1/.libs/libsecp256k1.a -lm
# ARM64 targets
arm64: $(ARM64_STATIC_LIB)
arm64-all: $(ARM64_STATIC_LIB)
# Debug build
debug: CFLAGS = $(DEBUG_CFLAGS)
debug: clean default
# Install library to system (static only)
install: $(STATIC_LIB)
@echo "Installing static library..."
sudo cp $(STATIC_LIB) /usr/local/lib/
sudo cp nostr_core/nostr_core.h /usr/local/include/
sudo cp nostr_core/nostr_crypto.h /usr/local/include/
# Uninstall library
uninstall:
@echo "Uninstalling library..."
sudo rm -f /usr/local/lib/$(STATIC_LIB)
sudo rm -f /usr/local/include/nostr_core.h
sudo rm -f /usr/local/include/nostr_crypto.h
# Test the library
test: examples/simple_keygen
@echo "Running simple key generation test..."
./examples/simple_keygen
# Run crypto tests
test-crypto:
@echo "Running comprehensive crypto test suite..."
cd tests && make test
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
rm -f $(LIB_OBJECTS) $(ARM64_LIB_OBJECTS)
rm -f $(STATIC_LIB) $(ARM64_STATIC_LIB)
rm -f $(EXAMPLE_TARGETS)
# Create distribution package
dist: clean
@echo "Creating distribution package..."
mkdir -p dist/nostr_core
cp -r *.h *.c Makefile examples/ tests/ README.md LICENSE dist/nostr_core/ 2>/dev/null || true
cd dist && tar -czf nostr_core.tar.gz nostr_core/
@echo "Distribution package created: dist/nostr_core.tar.gz"
# Help
help:
@echo "NOSTR Core Library Build System"
@echo "==============================="
@echo ""
@echo "Available targets:"
@echo " default - Build static library (recommended)"
@echo " all - Build static library and examples"
@echo " arm64 - Build ARM64 static library"
@echo " arm64-all - Build ARM64 static library"
@echo " debug - Build with debug symbols"
@echo " examples - Build example programs"
@echo " test - Run simple test"
@echo " test-crypto - Run comprehensive crypto test suite"
@echo " install - Install static library to system (/usr/local)"
@echo " uninstall - Remove library from system"
@echo " clean - Remove build artifacts"
@echo " dist - Create distribution package"
@echo " help - Show this help"
@echo ""
@echo "Library outputs (static only):"
@echo " $(STATIC_LIB) - x86_64 static library"
@echo " $(ARM64_STATIC_LIB) - ARM64 static library"
.PHONY: default all arm64 arm64-all debug examples test test-crypto install uninstall clean dist help
+777
View File
@@ -0,0 +1,777 @@
# Relay Pool API Reference
This document describes the public API for the Nostr Relay Pool implementation in [`core_relay_pool.c`](nostr_core/core_relay_pool.c).
## Function Summary
| Function | Description |
|----------|-------------|
| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:594) | Create and initialize a new relay pool |
| [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:733) | Destroy pool and cleanup all resources |
| [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:648) | Add a relay URL to the pool |
| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:702) | Remove a relay URL from the pool |
| [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) | Configure pool-wide NIP-42 authentication key and enable flag |
| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:778) | Create async subscription with callbacks |
| [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491) | Close subscription and free resources |
| [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192) | Run event loop for specified timeout |
| [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) | Single iteration poll and dispatch |
| [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695) | Synchronous query returning event array |
| [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825) | Get single most recent event |
| [`nostr_relay_pool_publish_async()`](nostr_core/core_relay_pool.c:866) | Publish event with async callbacks |
| [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944) | Get connection status for a relay |
| [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960) | List all relays and their statuses |
| [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992) | Get detailed statistics for a relay |
| [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008) | Reset statistics for a relay |
| [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045) | Get average query latency for a relay |
## Pool Lifecycle
### Create Pool
**Function:** [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219)
```c
nostr_relay_pool_t* nostr_relay_pool_create(void);
```
**Example:**
```c
#include "nostr_core.h"
int main() {
// Create a new relay pool
nostr_relay_pool_t* pool = nostr_relay_pool_create();
if (!pool) {
fprintf(stderr, "Failed to create relay pool\n");
return -1;
}
// Use the pool...
// Clean up
nostr_relay_pool_destroy(pool);
return 0;
}
```
### Destroy Pool
**Function:** [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304)
```c
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
```
**Example:**
```c
// Properly cleanup a relay pool
void cleanup_pool(nostr_relay_pool_t* pool) {
if (pool) {
// This will close all active subscriptions and relay connections
nostr_relay_pool_destroy(pool);
pool = NULL;
}
}
```
## Relay Management
### Configure Pool Authentication (NIP-42)
**Function:** [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616)
```c
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable);
```
**Description:**
- Sets a pool-wide private key used to sign NIP-42 `AUTH` responses.
- Enables/disables NIP-42 handling for all relays in the pool.
- Propagates auth-enabled state to existing relay connections.
**Example:**
```c
unsigned char privkey[32];
nostr_hex_to_bytes("91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", privkey, 32);
nostr_relay_pool_t* pool = nostr_relay_pool_create(nostr_pool_reconnect_config_default());
nostr_relay_pool_set_auth(pool, privkey, 1); // enable NIP-42 auto AUTH handling
```
### Add Relay
**Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229)
```c
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
```
**Example:**
```c
int setup_relays(nostr_relay_pool_t* pool) {
const char* relays[] = {
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
};
for (int i = 0; i < 3; i++) {
int result = nostr_relay_pool_add_relay(pool, relays[i]);
if (result != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to add relay %s: %d\n", relays[i], result);
return -1;
}
printf("Added relay: %s\n", relays[i]);
}
return 0;
}
```
### Remove Relay
**Function:** [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273)
```c
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
```
**Example:**
```c
int remove_slow_relay(nostr_relay_pool_t* pool) {
const char* slow_relay = "wss://slow-relay.example.com";
int result = nostr_relay_pool_remove_relay(pool, slow_relay);
if (result == NOSTR_SUCCESS) {
printf("Successfully removed relay: %s\n", slow_relay);
} else {
printf("Failed to remove relay %s (may not exist)\n", slow_relay);
}
return result;
}
```
## Subscriptions (Asynchronous)
### Subscribe to Events
**Function:** [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399)
```c
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data);
```
**Example:**
```c
#include "cjson/cJSON.h"
// Event callback - called for each received event
void handle_event(cJSON* event, const char* relay_url, void* user_data) {
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
if (content && pubkey) {
printf("Event from %s: %s (by %s)\n",
relay_url,
cJSON_GetStringValue(content),
cJSON_GetStringValue(pubkey));
}
}
// EOSE callback - called when all relays finish sending stored events
void handle_eose(void* user_data) {
printf("All relays finished sending stored events\n");
}
int subscribe_to_notes(nostr_relay_pool_t* pool) {
// Create filter for kind 1 (text notes) from last hour
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
time_t since = time(NULL) - 3600; // Last hour
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(50));
// Subscribe to specific relays
const char* relay_urls[] = {
"wss://relay.damus.io",
"wss://nos.lol"
};
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool,
relay_urls,
2,
filter,
handle_event,
handle_eose,
NULL // user_data
);
cJSON_Delete(filter); // Pool makes its own copy
if (!sub) {
fprintf(stderr, "Failed to create subscription\n");
return -1;
}
// Drive the event loop to receive events
printf("Listening for events for 30 seconds...\n");
nostr_relay_pool_run(pool, 30000); // 30 seconds
// Close subscription
nostr_pool_subscription_close(sub);
return 0;
}
```
### Close Subscription
**Function:** [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491)
```c
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
```
**Example:**
```c
// Subscription management with cleanup
typedef struct {
nostr_pool_subscription_t* subscription;
int event_count;
int should_stop;
} subscription_context_t;
void event_counter(cJSON* event, const char* relay_url, void* user_data) {
subscription_context_t* ctx = (subscription_context_t*)user_data;
ctx->event_count++;
printf("Received event #%d from %s\n", ctx->event_count, relay_url);
// Stop after 10 events
if (ctx->event_count >= 10) {
ctx->should_stop = 1;
}
}
int limited_subscription(nostr_relay_pool_t* pool) {
subscription_context_t ctx = {0};
// Create filter
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
const char* relay_urls[] = {"wss://relay.damus.io"};
ctx.subscription = nostr_relay_pool_subscribe(
pool, relay_urls, 1, filter, event_counter, NULL, &ctx);
cJSON_Delete(filter);
if (!ctx.subscription) {
return -1;
}
// Poll until we should stop
while (!ctx.should_stop) {
int events = nostr_relay_pool_poll(pool, 100);
if (events < 0) break;
}
// Clean up
int result = nostr_pool_subscription_close(ctx.subscription);
printf("Subscription closed with result: %d\n", result);
return 0;
}
```
## Event Loop
### Run Timed Loop
**Function:** [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192)
```c
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
```
**Example:**
```c
int run_event_loop(nostr_relay_pool_t* pool) {
printf("Starting event loop for 60 seconds...\n");
// Run for 60 seconds, processing all incoming events
int total_events = nostr_relay_pool_run(pool, 60000);
if (total_events < 0) {
fprintf(stderr, "Event loop error\n");
return -1;
}
printf("Processed %d events total\n", total_events);
return 0;
}
```
### Single Poll Iteration
**Function:** [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232)
```c
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
```
**Example:**
```c
// Integration with custom main loop
int custom_main_loop(nostr_relay_pool_t* pool) {
int running = 1;
int total_events = 0;
while (running) {
// Poll for Nostr events (non-blocking with 50ms timeout)
int events = nostr_relay_pool_poll(pool, 50);
if (events > 0) {
total_events += events;
printf("Processed %d events this iteration\n", events);
}
// Do other work in your application
// handle_ui_events();
// process_background_tasks();
// Check exit condition
// running = !should_exit();
// Simple exit after 100 events for demo
if (total_events >= 100) {
running = 0;
}
}
printf("Main loop finished, processed %d total events\n", total_events);
return 0;
}
```
## Synchronous Operations
### Query Multiple Events
**Function:** [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695)
```c
cJSON** nostr_relay_pool_query_sync(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int* event_count,
int timeout_ms);
```
**Example:**
```c
int query_recent_notes(nostr_relay_pool_t* pool) {
// Create filter for recent text notes
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(20));
const char* relay_urls[] = {
"wss://relay.damus.io",
"wss://nos.lol"
};
int event_count = 0;
cJSON** events = nostr_relay_pool_query_sync(
pool, relay_urls, 2, filter, &event_count, 10000); // 10 second timeout
cJSON_Delete(filter);
if (!events) {
printf("No events received or query failed\n");
return -1;
}
printf("Received %d events:\n", event_count);
for (int i = 0; i < event_count; i++) {
cJSON* content = cJSON_GetObjectItem(events[i], "content");
if (content) {
printf(" %d: %s\n", i + 1, cJSON_GetStringValue(content));
}
// Free each event
cJSON_Delete(events[i]);
}
// Free the events array
free(events);
return event_count;
}
```
### Get Single Most Recent Event
**Function:** [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825)
```c
cJSON* nostr_relay_pool_get_event(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int timeout_ms);
```
**Example:**
```c
int get_latest_note_from_pubkey(nostr_relay_pool_t* pool, const char* pubkey_hex) {
// Create filter for specific author's notes
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(1));
const char* relay_urls[] = {"wss://relay.damus.io"};
cJSON* event = nostr_relay_pool_get_event(
pool, relay_urls, 1, filter, 5000); // 5 second timeout
cJSON_Delete(filter);
if (!event) {
printf("No recent event found for pubkey %s\n", pubkey_hex);
return -1;
}
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
if (content && created_at) {
printf("Latest note: %s (created at %ld)\n",
cJSON_GetStringValue(content),
(long)cJSON_GetNumberValue(created_at));
}
cJSON_Delete(event);
return 0;
}
```
### Publish Event
**Function:** [`nostr_relay_pool_publish_async()`](nostr_core/core_relay_pool.c:866)
```c
int nostr_relay_pool_publish_async(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* event);
```
**Example:**
```c
int publish_text_note(nostr_relay_pool_t* pool, const char* content) {
// Create a basic text note event (this is simplified - real implementation
// would need proper signing with private key)
cJSON* event = cJSON_CreateObject();
cJSON_AddItemToObject(event, "kind", cJSON_CreateNumber(1));
cJSON_AddItemToObject(event, "content", cJSON_CreateString(content));
cJSON_AddItemToObject(event, "created_at", cJSON_CreateNumber(time(NULL)));
// In real usage, you'd add pubkey, id, sig fields here
cJSON_AddItemToObject(event, "pubkey", cJSON_CreateString("your_pubkey_hex"));
cJSON_AddItemToObject(event, "id", cJSON_CreateString("event_id_hash"));
cJSON_AddItemToObject(event, "sig", cJSON_CreateString("event_signature"));
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
const char* relay_urls[] = {
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
};
printf("Publishing note: %s\n", content);
int success_count = nostr_relay_pool_publish_async(
pool, relay_urls, 3, event, my_callback, user_data);
cJSON_Delete(event);
printf("Successfully published to %d out of 3 relays\n", success_count);
if (success_count == 0) {
fprintf(stderr, "Failed to publish to any relay\n");
return -1;
}
return success_count;
}
```
## Status and Statistics
### Get Relay Status
**Function:** [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944)
```c
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void check_relay_status(nostr_relay_pool_t* pool, const char* relay_url) {
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(pool, relay_url);
const char* status_str;
switch (status) {
case NOSTR_POOL_RELAY_DISCONNECTED:
status_str = "DISCONNECTED";
break;
case NOSTR_POOL_RELAY_CONNECTING:
status_str = "CONNECTING";
break;
case NOSTR_POOL_RELAY_CONNECTED:
status_str = "CONNECTED";
break;
case NOSTR_POOL_RELAY_ERROR:
status_str = "ERROR";
break;
default:
status_str = "UNKNOWN";
break;
}
printf("Relay %s status: %s\n", relay_url, status_str);
}
```
### List All Relays
**Function:** [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960)
```c
int nostr_relay_pool_list_relays(
nostr_relay_pool_t* pool,
char*** relay_urls,
nostr_pool_relay_status_t** statuses);
```
**Example:**
```c
void print_all_relays(nostr_relay_pool_t* pool) {
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (count < 0) {
printf("Failed to list relays\n");
return;
}
if (count == 0) {
printf("No relays configured\n");
return;
}
printf("Configured relays (%d):\n", count);
for (int i = 0; i < count; i++) {
const char* status_str = (statuses[i] == NOSTR_POOL_RELAY_CONNECTED) ?
"CONNECTED" : "DISCONNECTED";
printf(" %s - %s\n", relay_urls[i], status_str);
// Free the duplicated URL string
free(relay_urls[i]);
}
// Free the arrays
free(relay_urls);
free(statuses);
}
```
### Get Relay Statistics
**Function:** [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992)
```c
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void print_relay_stats(nostr_relay_pool_t* pool, const char* relay_url) {
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_url);
if (!stats) {
printf("No stats available for relay %s\n", relay_url);
return;
}
printf("Statistics for %s:\n", relay_url);
printf(" Connection attempts: %d\n", stats->connection_attempts);
printf(" Connection failures: %d\n", stats->connection_failures);
printf(" Events received: %d\n", stats->events_received);
printf(" Events published: %d\n", stats->events_published);
printf(" Events published OK: %d\n", stats->events_published_ok);
printf(" Events published failed: %d\n", stats->events_published_failed);
printf(" Query latency avg: %.2f ms\n", stats->query_latency_avg);
printf(" Query samples: %d\n", stats->query_samples);
printf(" Publish latency avg: %.2f ms\n", stats->publish_latency_avg);
printf(" Publish samples: %d\n", stats->publish_samples);
if (stats->last_event_time > 0) {
printf(" Last event: %ld seconds ago\n",
time(NULL) - stats->last_event_time);
}
}
```
### Reset Relay Statistics
**Function:** [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008)
```c
int nostr_relay_pool_reset_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void reset_stats_for_relay(nostr_relay_pool_t* pool, const char* relay_url) {
int result = nostr_relay_pool_reset_relay_stats(pool, relay_url);
if (result == NOSTR_SUCCESS) {
printf("Successfully reset statistics for %s\n", relay_url);
} else {
printf("Failed to reset statistics for %s\n", relay_url);
}
}
```
### Get Query Latency
**Function:** [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045)
```c
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void check_relay_performance(nostr_relay_pool_t* pool) {
const char* relays[] = {
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
};
printf("Relay performance comparison:\n");
for (int i = 0; i < 3; i++) {
double latency = nostr_relay_pool_get_relay_query_latency(pool, relays[i]);
if (latency >= 0) {
printf(" %s: %.2f ms average query latency\n", relays[i], latency);
} else {
printf(" %s: No latency data available\n", relays[i]);
}
}
}
```
## Complete Example Application
```c
#include "nostr_core.h"
#include "cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Global context for the example
typedef struct {
int event_count;
int max_events;
} app_context_t;
void on_text_note(cJSON* event, const char* relay_url, void* user_data) {
app_context_t* ctx = (app_context_t*)user_data;
cJSON* content = cJSON_GetObjectItem(event, "content");
if (content && cJSON_IsString(content)) {
printf("[%s] Note #%d: %s\n",
relay_url, ++ctx->event_count, cJSON_GetStringValue(content));
}
}
void on_subscription_complete(void* user_data) {
printf("All relays finished sending stored events\n");
}
int main() {
// Initialize pool
nostr_relay_pool_t* pool = nostr_relay_pool_create();
if (!pool) {
fprintf(stderr, "Failed to create relay pool\n");
return 1;
}
// Add relays
const char* relays[] = {
"wss://relay.damus.io",
"wss://nos.lol"
};
for (int i = 0; i < 2; i++) {
if (nostr_relay_pool_add_relay(pool, relays[i]) != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to add relay: %s\n", relays[i]);
}
}
// Create filter for recent text notes
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(10));
// Set up context
app_context_t ctx = {0, 10};
// Subscribe
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool, relays, 2, filter, on_text_note, on_subscription_complete, &ctx);
cJSON_Delete(filter);
if (!sub) {
fprintf(stderr, "Failed to create subscription\n");
nostr_relay_pool_destroy(pool);
return 1;
}
// Run event loop for 30 seconds
printf("Listening for events...\n");
nostr_relay_pool_run(pool, 30000);
// Print final stats
for (int i = 0; i < 2; i++) {
print_relay_stats(pool, relays[i]);
}
// Cleanup
nostr_pool_subscription_close(sub);
nostr_relay_pool_destroy(pool);
printf("Application finished. Received %d events total.\n", ctx.event_count);
return 0;
}
```
## Notes
- All functions are **not thread-safe**. Use from a single thread or add external synchronization.
- **Memory ownership**: The pool duplicates filters and URLs internally. Caller owns returned events and must free them.
- **Event deduplication** is applied pool-wide using a circular buffer of 1000 event IDs.
- **Ping functionality** is currently disabled in this build.
- **NIP-42 behavior**: With [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) enabled, the pool will respond to relay `AUTH` challenges automatically using [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84).
- **Reconnection** happens on-demand when sending, but active subscriptions are not automatically re-sent after reconnect.
- **Polling model**: You must drive the event loop via [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1745) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1785) to receive events.
+603
View File
@@ -0,0 +1,603 @@
# NOSTR Core Library
A C library for NOSTR protocol implementation. Work in progress.
[![Version](https://img.shields.io/badge/version-0.4.8-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)
## 📋 NIP Implementation Status
### Core Protocol NIPs
- [x] [NIP-01](nips/01.md) - Basic protocol flow - event creation, signing, and validation
- [ ] [NIP-02](nips/02.md) - Contact List and Petnames
- [ ] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
- [x] [NIP-04](nips/04.md) - Encrypted Direct Messages (legacy)
- [x] [NIP-05](nips/05.md) - Mapping Nostr keys to DNS-based internet identifiers
- [x] [NIP-06](nips/06.md) - Basic key derivation from mnemonic seed phrase
- [ ] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
- [ ] [NIP-08](nips/08.md) - Handling Mentions
- [ ] [NIP-09](nips/09.md) - Event Deletion
- [ ] [NIP-10](nips/10.md) - Conventions for clients' use of `e` and `p` tags in text events
- [x] [NIP-11](nips/11.md) - Relay Information Document
- [ ] [NIP-12](nips/12.md) - Generic Tag Queries
- [x] [NIP-13](nips/13.md) - Proof of Work
- [ ] [NIP-14](nips/14.md) - Subject tag in text events
- [ ] [NIP-15](nips/15.md) - Nostr Marketplace (for resilient marketplaces)
- [ ] [NIP-16](nips/16.md) - Event Treatment
- [x] [NIP-17](nips/17.md) - Private Direct Messages
- [ ] [NIP-18](nips/18.md) - Reposts
- [x] [NIP-19](nips/19.md) - bech32-encoded entities
- [ ] [NIP-20](nips/20.md) - Command Results
- [x] [NIP-21](nips/21.md) - `nostr:` URI scheme
- [ ] [NIP-22](nips/22.md) - Event `created_at` Limits
- [ ] [NIP-23](nips/23.md) - Long-form Content
- [ ] [NIP-24](nips/24.md) - Extra metadata fields and tags
- [ ] [NIP-25](nips/25.md) - Reactions
- [ ] [NIP-26](nips/26.md) - Delegated Event Signing
- [ ] [NIP-27](nips/27.md) - Text Note References
- [ ] [NIP-28](nips/28.md) - Public Chat
- [ ] [NIP-29](nips/29.md) - Relay-based Groups
- [ ] [NIP-30](nips/30.md) - Custom Emoji
- [ ] [NIP-31](nips/31.md) - Dealing with Unknown Events
- [ ] [NIP-32](nips/32.md) - Labeling
- [ ] [NIP-33](nips/33.md) - Parameterized Replaceable Events
- [ ] [NIP-34](nips/34.md) - `git` stuff
- [ ] [NIP-35](nips/35.md) - Torrents
- [ ] [NIP-36](nips/36.md) - Sensitive Content / Content Warning
- [ ] [NIP-37](nips/37.md) - Draft Events
- [ ] [NIP-38](nips/38.md) - User Statuses
- [ ] [NIP-39](nips/39.md) - External Identities in Profiles
- [ ] [NIP-40](nips/40.md) - Expiration Timestamp
- [x] [NIP-42](nips/42.md) - Authentication of clients to relays
- [x] [NIP-44](nips/44.md) - Versioned Encryption
- [ ] [NIP-45](nips/45.md) - Counting results
- [x] [NIP-46](nips/46.md) - Nostr Remote Signing
- [ ] [NIP-47](nips/47.md) - Wallet Connect
- [ ] [NIP-48](nips/48.md) - Proxy Tags
- [ ] [NIP-49](nips/49.md) - Private Key Encryption
- [ ] [NIP-50](nips/50.md) - Search Capability
- [ ] [NIP-51](nips/51.md) - Lists
- [ ] [NIP-52](nips/52.md) - Calendar Events
- [ ] [NIP-53](nips/53.md) - Live Activities
- [ ] [NIP-54](nips/54.md) - Wiki
- [ ] [NIP-55](nips/55.md) - Android Signer Application
- [ ] [NIP-56](nips/56.md) - Reporting
- [ ] [NIP-57](nips/57.md) - Lightning Zaps
- [ ] [NIP-58](nips/58.md) - Badges
- [x] [NIP-59](nips/59.md) - Gift Wrap
- [ ] [NIP-60](nips/60.md) - Cashu Wallet
- [ ] [NIP-61](nips/61.md) - Nutzaps
- [ ] [NIP-62](nips/62.md) - Log events
- [ ] [NIP-64](nips/64.md) - Chess (PGN)
- [ ] [NIP-65](nips/65.md) - Relay List Metadata
- [ ] [NIP-66](nips/66.md) - Relay Monitor
- [ ] [NIP-68](nips/68.md) - Web badges
- [ ] [NIP-69](nips/69.md) - Peer-to-peer Order events
- [ ] [NIP-70](nips/70.md) - Protected Events
- [ ] [NIP-71](nips/71.md) - Video Events
- [ ] [NIP-72](nips/72.md) - Moderated Communities
- [ ] [NIP-73](nips/73.md) - External Content IDs
- [ ] [NIP-75](nips/75.md) - Zap Goals
- [ ] [NIP-77](nips/77.md) - Arbitrary custom app data
- [ ] [NIP-78](nips/78.md) - Application-specific data
- [ ] [NIP-84](nips/84.md) - Highlights
- [ ] [NIP-86](nips/86.md) - Relay Management API
- [ ] [NIP-87](nips/87.md) - Relay List Recommendations
- [ ] [NIP-88](nips/88.md) - Stella: A Stellar Relay
- [ ] [NIP-89](nips/89.md) - Recommended Application Handlers
- [ ] [NIP-90](nips/90.md) - Data Vending Machines
- [ ] [NIP-92](nips/92.md) - Media Attachments
- [ ] [NIP-94](nips/94.md) - File Metadata
- [ ] [NIP-96](nips/96.md) - HTTP File Storage Integration
- [ ] [NIP-98](nips/98.md) - HTTP Auth
- [ ] [NIP-99](nips/99.md) - Classified Listings
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
## 📦 Quick Start
### Installation
1. **Clone the repository:**
```bash
git clone https://github.com/yourusername/nostr_core_lib.git
cd nostr_core_lib
```
2. **Build the library:**
```bash
./build.sh
```
3. **Build and run examples:**
```bash
./build.sh -e
./examples/simple_keygen
```
### Usage Example
```c
#include "nostr_core/nostr_core.h"
#include <stdio.h>
int main() {
// Initialize library
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize NOSTR library\n");
return 1;
}
// Generate keypair
unsigned char private_key[32], public_key[32];
nostr_generate_keypair(private_key, public_key);
// Convert to bech32 format
char nsec[100], npub[100];
nostr_key_to_bech32(private_key, "nsec", nsec);
nostr_key_to_bech32(public_key, "npub", npub);
printf("Private key: %s\n", nsec);
printf("Public key: %s\n", npub);
// Create and sign event
cJSON* event = nostr_create_and_sign_event(1, "Hello NOSTR!", NULL, private_key, 0);
if (event) {
char* json = cJSON_Print(event);
printf("Event: %s\n", json);
free(json);
cJSON_Delete(event);
}
nostr_cleanup();
return 0;
}
```
**Compile and run:**
```bash
gcc example.c -o example ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
./example
```
## 🏗️ Building
### Build Targets
```bash
./build.sh # Build static library (default)
./build.sh --nips=all # Force build with all available NIPs
./build.sh -t # Build all test executables in tests/
./build.sh -e # Build all examples in examples/
./build.sh -t -e # Build library + tests + examples
./build.sh --nips=46 # Build specifically with NIP-046 support
./build.sh --help # Show all options
```
### Dependencies
**Required System Dependencies:**
- GCC or compatible C compiler
- Standard C library and math library (`-lm`)
- OpenSSL development libraries (`-lssl -lcrypto`)
- curl development libraries (`-lcurl`)
- secp256k1 development libraries (`-lsecp256k1`)
**Install on Ubuntu/Debian:**
```bash
sudo apt install build-essential libssl-dev libcurl4-openssl-dev libsecp256k1-dev
```
**Install on CentOS/RHEL:**
```bash
sudo yum install gcc openssl-devel libcurl-devel libsecp256k1-devel
```
**Install on macOS:**
```bash
brew install openssl curl secp256k1
```
**Still Bundled:**
- cJSON (JSON parsing - internal copy)
- TinyAES-c (AES encryption for NIP-04)
- ChaCha20 (stream cipher for NIP-44)
## 📚 API Documentation
### Initialization
```c
int nostr_init(void); // Initialize library (call first)
void nostr_cleanup(void); // Cleanup resources (call last)
const char* nostr_strerror(int error); // Get error message
```
### Key Management
```c
// Generate random keypair
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key);
// Generate from mnemonic
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key);
// Derive from existing mnemonic
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key);
// Format conversion
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output);
nostr_input_type_t nostr_detect_input_type(const char* input);
```
### Event Creation
```c
// Create and sign event
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags,
const unsigned char* private_key, time_t timestamp);
// Add proof of work
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty, void (*progress_callback)(...), void* user_data);
```
### Encryption (NIP-04 & NIP-44)
```c
// NIP-04 (AES-CBC)
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext, char* output, size_t output_size);
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data, char* output, size_t output_size);
// NIP-44 (ChaCha20)
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext, char* output, size_t output_size);
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data, char* output, size_t output_size);
```
### Relay Communication
```c
// Simple relay query
cJSON* nostr_query_relay_for_event(const char* relay_url, const char* pubkey_hex, int kind);
// Multi-relay synchronous queries
cJSON** synchronous_query_relays_with_progress(const char** relay_urls, int relay_count,
cJSON* filter, relay_query_mode_t mode,
int* result_count, int relay_timeout_seconds,
relay_progress_callback_t callback, void* user_data);
// Multi-relay publishing
publish_result_t* synchronous_publish_event_with_progress(const char** relay_urls, int relay_count,
cJSON* event, int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback, void* user_data);
```
### Relay Pools (Asynchronous)
```c
// Create and manage relay pool with reconnection
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config);
int nostr_relay_pool_add_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);
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
// Subscribe to events (with auto-reconnection)
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool, const char** relay_urls, int relay_count, cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data), void* user_data, int close_on_eose);
// Enable NIP-42 auth (auto-responds to relay AUTH challenges)
unsigned char private_key[32];
nostr_hex_to_bytes("91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", private_key, 32);
nostr_relay_pool_set_auth(pool, private_key, 1);
// Run event loop (handles reconnection + incoming AUTH/NOTICE/OK messages)
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
// Reconnection configuration
typedef struct {
int enable_auto_reconnect; // Enable automatic reconnection
int max_reconnect_attempts; // Maximum retry attempts
int initial_reconnect_delay_ms; // Initial delay between attempts
int max_reconnect_delay_ms; // Maximum delay cap
int reconnect_backoff_multiplier; // Exponential backoff factor
int ping_interval_seconds; // Health check ping interval
int pong_timeout_seconds; // Pong response timeout
} nostr_pool_reconnect_config_t;
```
### NIP-05 Identifier Verification
```c
// Lookup public key from NIP-05 identifier
int nostr_nip05_lookup(const char* nip05_identifier, char* pubkey_hex_out,
char*** relays, int* relay_count, int timeout_seconds);
// Verify NIP-05 identifier against public key
int nostr_nip05_verify(const char* nip05_identifier, const char* pubkey_hex,
char*** relays, int* relay_count, int timeout_seconds);
// Parse .well-known/nostr.json response
int nostr_nip05_parse_well_known(const char* json_response, const char* local_part,
char* pubkey_hex_out, char*** relays, int* relay_count);
```
### NIP-11 Relay Information
```c
// Fetch relay information document
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds);
// Free relay information structure
void nostr_nip11_relay_info_free(nostr_relay_info_t* info);
```
## 📁 Examples
The library includes comprehensive examples:
- **`simple_keygen`** - Basic key generation and formatting
- **`keypair_generation`** - Advanced key management
- **`mnemonic_generation`** - BIP39 mnemonic handling
- **`mnemonic_derivation`** - NIP-06 key derivation
- **`utility_functions`** - General utility demonstrations
- **`input_detection`** - Input type detection and processing
- **`relay_pool`** - Interactive relay pool and subscription testing
- **`send_nip17_dm`** - NIP-17 private direct message send flow
- **`nip46_remote_signer`** - NIP-46 remote signer flow and bunker URL generation
- **`version_test`** - Library version information
Build all examples:
```bash
./build.sh -e
ls -la examples/
```
## 🧪 Testing
The library includes extensive tests:
```bash
# Build all test executables
./build.sh -t
# Run the new NIP-46 test
./tests/nip46_test
# Run selected tests
./tests/nip44_test
./tests/nip42_test
./tests/nip17_test
```
**Current test binaries live in [`tests/`](tests/) and are generated from `*_test.c` sources.**
## 🏗️ Integration
### Static Library Integration (Recommended)
1. **Install system dependencies first** (see Dependencies section above)
2. **Copy required files to your project:**
```bash
cp libnostr_core_x64.a /path/to/your/project/
cp nostr_core/nostr_core.h /path/to/your/project/
```
3. **Link in your project:**
```bash
gcc your_code.c -L. -lnostr_core -lssl -lcrypto -lcurl -lsecp256k1 -lm -o your_program
```
### Source Integration
1. **Install system dependencies first** (see Dependencies section above)
2. **Copy source files:**
```bash
cp -r nostr_core/ /path/to/your/project/
cp -r cjson/ /path/to/your/project/
```
3. **Include in your build:**
```bash
gcc your_code.c nostr_core/*.c cjson/cJSON.c -lssl -lcrypto -lcurl -lsecp256k1 -lm -o your_program
```
### System Dependencies Library
The `libnostr_core.a` library now uses **system dependencies** for all major crypto libraries:
- ✅ **Uses system OpenSSL** (`-lssl -lcrypto`)
- ✅ **Uses system curl** (`-lcurl`)
- ✅ **Uses system secp256k1** (`-lsecp256k1`)
- ✅ **Includes only internal code** (cJSON, TinyAES, ChaCha20)
**Complete linking example:**
```bash
gcc your_app.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1 -o your_app
```
**Check system dependencies:**
```bash
ldd your_app # Shows linked system libraries
```
## 🔧 Configuration
### Compile-Time Options
```c
// Enable debug output
#define NOSTR_DEBUG_ENABLED
// Crypto-only build (no networking)
#define NOSTR_CRYPTO_ONLY
// Enable specific NIPs
#define NOSTR_NIP04_ENABLED
#define NOSTR_NIP44_ENABLED
#define NOSTR_NIP13_ENABLED
```
### Build Flags
```bash
# Enable websocket and PoW debug emission (now callback-based)
make LOGGING_FLAGS="-DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
# Debug build
make debug
# ARM64 cross-compile
make arm64
```
### Logging Integration (Consumer-Controlled)
`nostr_core_lib` now supports a callback logging API so host applications can route library logs into their own logger and destination.
- API is exposed via [`nostr_core/nostr_log.h`](nostr_core/nostr_log.h)
- Included automatically from [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h)
```c
#include "nostr_core/nostr_core.h"
static void my_log_cb(int level, const char* component, const char* message, void* user_data) {
(void)user_data;
fprintf(stderr, "[nostr][%s][%d] %s\n", component ? component : "unknown", level, message ? message : "");
}
int main(void) {
if (nostr_init() != NOSTR_SUCCESS) return 1;
nostr_set_log_callback(my_log_cb, NULL);
nostr_set_log_level(NOSTR_LOG_LEVEL_TRACE);
/* ... your code ... */
nostr_cleanup();
return 0;
}
```
#### Migration Notes
- Direct file logging to `debug.log` has been removed from websocket and NIP-13 internals.
- To receive logs, register a callback with [`nostr_set_log_callback()`](nostr_core/nostr_log.h:24).
- Use [`nostr_set_log_level()`](nostr_core/nostr_log.h:25) to reduce verbosity in production.
## 🌐 Supported Platforms
- **Linux** (x86_64, ARM64)
- **macOS** (Intel, Apple Silicon)
- **Windows** (MinGW, MSYS2)
- **Embedded Systems** (resource-constrained environments)
## 📄 Documentation
- **[POOL_API.md](POOL_API.md)** - Relay pool API notes
- **[nostr_websocket/README.md](nostr_websocket/README.md)** - WebSocket module details
- **[nostr_websocket/EXPORT_GUIDE.md](nostr_websocket/EXPORT_GUIDE.md)** - WebSocket export instructions
- **API Reference** - Complete documentation in [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h)
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Make your changes and add tests
4. Build tests and run key suites: `./build.sh -t && ./tests/nip46_test`
5. Commit your changes: `git commit -m 'Add amazing feature'`
6. Push to the branch: `git push origin feature/amazing-feature`
7. Open a Pull Request
## 📈 Version History
Current version: **0.4.8**
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
- **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.2.x` - Earlier OpenSSL-based development releases
- `v0.1.x` - Initial development releases
- Focus on core protocol implementation and OpenSSL-based crypto
- Full NIP-01, NIP-04, NIP-05, NIP-06, NIP-11, NIP-13, NIP-17, NIP-19, NIP-21, NIP-42, NIP-44, NIP-46, NIP-59 support
## 🐛 Troubleshooting
### Common Issues
**Build fails with secp256k1 errors:**
```bash
# Install secp256k1 with Schnorr support
sudo apt install libsecp256k1-dev # Ubuntu/Debian
# or
sudo yum install libsecp256k1-devel # CentOS/RHEL
# or
brew install secp256k1 # macOS
# If still failing, build from source with Schnorr support:
git clone https://github.com/bitcoin-core/secp256k1.git
cd secp256k1
./autogen.sh
./configure --enable-module-schnorrsig --enable-module-ecdh
make
sudo make install
```
**Library size:**
The library is small (~500KB) as it links against system libraries (secp256k1, OpenSSL, curl) rather than including them statically. This keeps the binary size manageable while maintaining full functionality.
**Linking errors:**
Make sure to include the math library:
```bash
gcc your_code.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
```
### Getting Help
- Check the `examples/` directory for working code
- Run `./build.sh test` to verify your environment
- Review the comprehensive API documentation in `nostr_core/nostr_core.h`
## 📜 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- **NOSTR Protocol** - The decentralized social media protocol
- **OpenSSL** - Production-grade cryptographic library and TLS implementation
- **secp256k1** - Bitcoin's elliptic curve library
- **cJSON** - Lightweight JSON parser
- **curl** - HTTP/HTTPS client library for NIP-05/NIP-11
- **NOSTR Community** - For protocol specification and feedback
---
**Built with ❤️ for the decentralized web**
*OpenSSL-based • Minimal dependencies • Work in progress*
+1 -1
View File
@@ -1 +1 @@
1.0.157
0.5.0
-122
View File
@@ -1,122 +0,0 @@
# Version Management System
This project implements an automated version management system for C applications with auto-incrementing build numbers.
## Overview
The version management system consists of:
- **Semantic versioning** (Major.Minor.Patch) in `VERSION` file
- **Auto-incrementing build numbers** in `.build_number` file
- **Generated version header** (`version.h`) with all version info
- **Build-time integration** via `scripts/generate_version.sh`
## Files
### Core Files
- `VERSION` - Contains semantic version (e.g., "1.0.0")
- `.build_number` - Contains current build number (auto-incremented)
- `scripts/generate_version.sh` - Version generation script
- `version.h` - Generated header (auto-created, do not edit)
### Integration Files
- `build.sh` - Modified to call version generation
- `.gitignore` - Excludes generated `version.h`
## Usage
### Building
Every time you run `./build.sh`, the build number automatically increments:
```bash
./build.sh # Build #1
./build.sh # Build #2
./build.sh # Build #3
```
### Version Display
The application shows version information in multiple ways:
**Command Line:**
```bash
./build/c_nostr --version
./build/c_nostr -v
```
**Interactive Mode:**
- ASCII art title with version
- Header shows: `NOSTR TERMINAL v1.0.0 (Build #3, 2025-07-21)`
### Updating Semantic Version
To update the major/minor/patch version:
```bash
echo "1.1.0" > VERSION
```
The next build will be `v1.1.0 (Build #4)`.
## Generated Macros
The `version.h` file contains these macros:
```c
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
#define VERSION_PATCH 0
#define VERSION_BUILD 3
#define VERSION_STRING "1.0.0"
#define VERSION_FULL "1.0.0.3"
#define BUILD_NUMBER 3
#define BUILD_DATE "2025-07-21"
#define BUILD_TIME "14:53:32"
#define BUILD_TIMESTAMP "2025-07-21 14:53:32"
#define GIT_HASH ""
#define GIT_BRANCH ""
#define VERSION_DISPLAY "v1.0.0 (Build #3)"
#define VERSION_FULL_DISPLAY "v1.0.0 (Build #3, 2025-07-21)"
```
## Integration in Code
Include the version header:
```c
#include "version.h"
```
Use version macros:
```c
printf("NOSTR TERMINAL %s\n", VERSION_FULL_DISPLAY);
printf("Built: %s\n", BUILD_TIMESTAMP);
```
## Best Practices
1. **Never edit `version.h`** - it's auto-generated
2. **Commit `.build_number`** - tracks build history
3. **Update `VERSION` manually** - for semantic version changes
4. **Build increments automatically** - no manual intervention needed
## Version History
Build numbers are persistent and increment across sessions:
- Build #1: Initial implementation
- Build #2: First rebuild test
- Build #3: Added --version flag
- Build #4: Next build...
## ASCII Art Integration
The version system integrates with the ASCII art title display:
```
███ ██ ██████ ███████ ████████ ███████ ██████
████ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ███████ ██ █████ ██████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ████ ███████ ██████ ███████ ███████ ██ ███████ ██ ██
NOSTR TERMINAL v1.0.0 (Build #3, 2025-07-21) - user_abc123
================================================================
```
This provides a professional, branded experience with clear version identification.
-116
View File
@@ -1,116 +0,0 @@
# Compiler Warning Cleanup - SUCCESS REPORT
## 🎉 All Warnings Resolved!
The nostr_core_lib now compiles with **zero compiler warnings** using `-Wall -Wextra` flags.
## ✅ Fixed Issues Summary
### 1. **Type Limits Warning** - `nostr_core/core.c`
- **Issue**: `comparison is always false due to limited range of data type [-Wtype-limits]`
- **Location**: `bech32_decode()` function, line 791
- **Problem**: Comparing `char c < 0` when `char` might be unsigned
- **Fix**: Changed `char c` to `unsigned char c` and removed redundant comparison
- **Status**: ✅ RESOLVED
### 2. **Unused Parameter Warning** - `nostr_core/nostr_crypto.c`
- **Issue**: `unused parameter 'mnemonic_size' [-Wunused-parameter]`
- **Location**: `nostr_bip39_mnemonic_from_bytes()` function
- **Problem**: Function parameter was declared but never used
- **Fix**: Removed `mnemonic_size` parameter from function signature and all call sites
- **Files Updated**:
- `nostr_core/nostr_crypto.c` (function implementation)
- `nostr_core/nostr_crypto.h` (function declaration)
- `nostr_core/core.c` (function call site)
- **Status**: ✅ RESOLVED
### 3. **Unused Constant Variable** - `nostr_core/nostr_crypto.c`
- **Issue**: `'CURVE_N' defined but not used [-Wunused-const-variable=]`
- **Location**: Line 456
- **Problem**: Constant array was defined but never referenced
- **Fix**: Removed the unused `CURVE_N` constant definition
- **Status**: ✅ RESOLVED
### 4. **Unused Variables** - `nostr_websocket/nostr_websocket_mbedtls.c`
- **Issue 1**: `unused variable 'tcp' [-Wunused-variable]` in `tcp_cleanup()`
- **Issue 2**: `unused variable 'fin' [-Wunused-variable]` in `ws_receive_frame()`
- **Fix**: Removed both unused variable declarations
- **Status**: ✅ RESOLVED
### 5. **Sign Comparison Warnings** - `nostr_websocket/nostr_websocket_mbedtls.c`
- **Issue**: `comparison of integer expressions of different signedness [-Wsign-compare]`
- **Locations**:
- `ws_parse_url()` - `path_start - url` vs `strlen(url)`
- `ws_perform_handshake()` - `len` vs `sizeof(request)`
- `ws_perform_handshake()` - `total_received` vs `sizeof(response) - 1`
- **Fix**: Added explicit casts to `size_t` for signed integers before comparison
- **Status**: ✅ RESOLVED
### 6. **Unused Function Warning** - `nostr_websocket/nostr_websocket_mbedtls.c`
- **Issue**: `'debug_log_cleanup' defined but not used [-Wunused-function]`
- **Problem**: Function was defined but never called
- **Fix**: Removed the unused function and its forward declaration
- **Status**: ✅ RESOLVED
## 🧪 Verification Results
### Clean Build Test
```bash
make clean && make
```
**Result**: ✅ **ZERO WARNINGS** - Clean compilation
### Functionality Test
```bash
make examples && ./examples/simple_keygen
```
**Result**: ✅ **ALL EXAMPLES WORK** - Library functionality preserved
## 📊 Before vs After
### Before Cleanup:
```
Compiling: nostr_core/core.c
nostr_core/core.c:791:24: warning: comparison is always false due to limited range of data type [-Wtype-limits]
Compiling: nostr_core/nostr_crypto.c
nostr_core/nostr_crypto.c:901:59: warning: unused parameter 'mnemonic_size' [-Wunused-parameter]
nostr_core/nostr_crypto.c:456:23: warning: 'CURVE_N' defined but not used [-Wunused-const-variable=]
Compiling: nostr_websocket/nostr_websocket_mbedtls.c
nostr_websocket/nostr_websocket_mbedtls.c:485:22: warning: unused variable 'tcp' [-Wunused-variable]
nostr_websocket/nostr_websocket_mbedtls.c:760:40: warning: operand of '?:' changes signedness [-Wsign-compare]
nostr_websocket/nostr_websocket_mbedtls.c:807:13: warning: comparison of integer expressions of different signedness [-Wsign-compare]
nostr_websocket/nostr_websocket_mbedtls.c:824:27: warning: comparison of integer expressions of different signedness [-Wsign-compare]
nostr_websocket/nostr_websocket_mbedtls.c:919:13: warning: unused variable 'fin' [-Wunused-variable]
nostr_websocket/nostr_websocket_mbedtls.c:1024:13: warning: 'debug_log_cleanup' defined but not used [-Wunused-function]
Total: 9 warnings
```
### After Cleanup:
```
Compiling: nostr_core/core.c
Compiling: nostr_core/core_relays.c
Compiling: nostr_core/nostr_crypto.c
Compiling: nostr_core/nostr_secp256k1.c
Compiling: cjson/cJSON.c
Compiling: nostr_websocket/nostr_websocket_mbedtls.c
Creating static library: libnostr_core.a
Total: 0 warnings ✅
```
## 🎯 Benefits Achieved
1. **Professional Code Quality**: Clean compilation with strict compiler flags
2. **Maintainability**: Removed unused code reduces confusion for future developers
3. **Portability**: Fixed sign comparison issues improve cross-platform compatibility
4. **Performance**: Compiler can better optimize warning-free code
5. **Debugging**: Cleaner build output makes real issues more visible
## 🏆 Final Status: **COMPLETE SUCCESS**
The nostr_core_lib now compiles cleanly with zero warnings while maintaining full functionality. All examples continue to work correctly, demonstrating that the cleanup did not introduce any regressions.
**Mission Accomplished!** 🚀
+719 -134
View File
@@ -1,161 +1,746 @@
#!/bin/bash
# NOSTR Core Library Build Script
# Provides convenient build targets for the standalone library
# NOSTR Core Library - Customer Build Script with Auto-Detection
# Automatically detects which NIPs are needed based on #include statements
set -e # Exit on any error
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Color constants
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
BOLD='\033[1m'
RESET='\033[0m'
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
# Detect if we should use colors (terminal output and not piped)
USE_COLORS=true
if [ ! -t 1 ] || [ "$NO_COLOR" = "1" ]; then
USE_COLORS=false
fi
# Function to print output with colors
print_info() {
if [ "$USE_COLORS" = true ]; then
echo -e "${BLUE}[INFO]${RESET} $1"
else
echo "[INFO] $1"
fi
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
if [ "$USE_COLORS" = true ]; then
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
else
echo "[SUCCESS] $1"
fi
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
if [ "$USE_COLORS" = true ]; then
echo -e "${YELLOW}[WARNING]${RESET} $1"
else
echo "[WARNING] $1"
fi
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
if [ "$USE_COLORS" = true ]; then
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
else
echo "[ERROR] $1"
fi
}
# Function to show usage
show_usage() {
echo "NOSTR Core Library Build Script"
echo "==============================="
echo ""
echo "Usage: $0 [target]"
echo ""
echo "Available targets:"
echo " clean - Clean all build artifacts"
echo " lib - Build static library (default)"
echo " shared - Build shared library"
echo " all - Build both static and shared libraries"
echo " examples - Build example programs"
echo " test - Run tests"
echo " install - Install library to system"
echo " uninstall - Remove library from system"
echo " help - Show this help message"
echo ""
echo "Library outputs:"
echo " libnostr_core.a - Static library"
echo " libnostr_core.so - Shared library"
echo " examples/* - Example programs"
}
# Default values
ARCHITECTURE=""
FORCE_NIPS=""
VERBOSE=false
HELP=false
BUILD_TESTS=false
BUILD_EXAMPLES=false
NO_COLOR_FLAG=false
# Parse command line arguments
TARGET=${1:-lib}
while [[ $# -gt 0 ]]; do
case $1 in
x64|x86_64|amd64)
ARCHITECTURE="x64"
shift
;;
arm64|aarch64)
ARCHITECTURE="arm64"
shift
;;
--nips=*)
FORCE_NIPS="${1#*=}"
shift
;;
--verbose|-v)
VERBOSE=true
shift
;;
--tests|-t)
BUILD_TESTS=true
shift
;;
--examples|-e)
BUILD_EXAMPLES=true
shift
;;
--no-color)
NO_COLOR_FLAG=true
shift
;;
--help|-h)
HELP=true
shift
;;
*)
print_error "Unknown argument: $1"
HELP=true
shift
;;
esac
done
case "$TARGET" in
clean)
print_status "Cleaning build artifacts..."
make clean
print_success "Clean completed"
;;
lib|library)
print_status "Building static library..."
make clean
make
if [ -f "libnostr_core.a" ]; then
SIZE=$(stat -c%s "libnostr_core.a")
print_success "Static library built successfully (${SIZE} bytes)"
ls -la libnostr_core.a
else
print_error "Failed to build static library"
exit 1
fi
;;
shared)
print_status "Building shared library..."
make clean
make libnostr_core.so
if [ -f "libnostr_core.so" ]; then
SIZE=$(stat -c%s "libnostr_core.so")
print_success "Shared library built successfully (${SIZE} bytes)"
ls -la libnostr_core.so
else
print_error "Failed to build shared library"
exit 1
fi
;;
all)
print_status "Building all libraries..."
make clean
make all
print_success "All libraries built successfully"
ls -la libnostr_core.*
;;
examples)
print_status "Building examples..."
make clean
make
make examples
print_success "Examples built successfully"
ls -la examples/
;;
test)
print_status "Running tests..."
make clean
make
if make test-crypto 2>/dev/null; then
print_success "All tests passed"
else
print_warning "Running simple test instead..."
make test
print_success "Basic test completed"
fi
;;
tests)
print_status "Running tests..."
make clean
make
if make test-crypto 2>/dev/null; then
print_success "All tests passed"
else
print_warning "Running simple test instead..."
make test
print_success "Basic test completed"
fi
;;
# Apply no-color flag
if [ "$NO_COLOR_FLAG" = true ]; then
USE_COLORS=false
fi
install)
print_status "Installing library to system..."
make clean
make all
sudo make install
print_success "Library installed to /usr/local"
;;
# Show help
if [ "$HELP" = true ]; then
echo "NOSTR Core Library - Customer Build Script"
echo ""
echo "Usage: $0 [architecture] [options]"
echo ""
echo "Architectures:"
echo " x64, x86_64, amd64 Build for x86-64 architecture"
echo " arm64, aarch64 Build for ARM64 architecture"
echo " (default) Build for current architecture"
echo ""
echo "Options:"
echo " --nips=1,5,6,19 Force specific NIPs (comma-separated)"
echo " --nips=all Include all available NIPs"
echo " --tests, -t Build all test programs in tests/ directory"
echo " --examples, -e Build all example programs in examples/ directory"
echo " --verbose, -v Verbose output"
echo " --no-color Disable colored output"
echo " --help, -h Show this help"
echo ""
echo "Auto-Detection:"
echo " Script automatically scans your .c files for #include statements"
echo " like #include \"nip001.h\" and compiles only needed NIPs."
echo ""
echo "Available NIPs:"
echo " 001 - Basic Protocol (event creation, signing)"
echo " 004 - Encryption (legacy)"
echo " 005 - DNS-based identifiers"
echo " 006 - Key derivation from mnemonic"
echo " 011 - Relay information document"
echo " 013 - Proof of Work"
echo " 017 - Private Direct Messages"
echo " 019 - Bech32 encoding (nsec/npub)"
echo " 021 - nostr: URI scheme"
echo " 042 - Authentication of clients to relays"
echo " 044 - Encryption (modern)"
echo " 046 - Remote signing"
echo " 059 - Gift Wrap"
echo " 060 - Cashu Wallet"
echo " 061 - Nutzaps"
echo ""
echo "Examples:"
echo " $0 # Auto-detect NIPs, build for current arch"
echo " $0 x64 # Auto-detect NIPs, build for x64"
echo " $0 --nips=1,6,19 # Force NIPs 1,6,19 only"
echo " $0 arm64 --nips=all # Build all NIPs for ARM64"
echo " $0 -t # Build library and all tests"
exit 0
fi
print_info "NOSTR Core Library - Customer Build Script"
# Check if we're running from the correct directory
CURRENT_DIR=$(basename "$(pwd)")
if [ "$CURRENT_DIR" != "nostr_core_lib" ]; then
print_error "Build script must be run from the nostr_core_lib directory"
echo ""
echo "Current directory: $CURRENT_DIR"
echo "Expected directory: nostr_core_lib"
echo ""
echo "Please change to the nostr_core_lib directory first, then run the build script."
echo ""
echo "Correct usage:"
echo " cd nostr_core_lib"
echo " ./build.sh"
echo ""
echo "Or if nostr_core_lib is in your project directory:"
echo " cd nostr_core_lib"
echo " ./build.sh"
echo " cd .."
echo " gcc your_app.c nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1 -o your_app"
echo ""
exit 1
fi
print_info "Auto-detecting needed NIPs from your source code..."
###########################################################################################
###########################################################################################
############ AUTODETECT NIPS FROM SOURCE FILES
###########################################################################################
###########################################################################################
NEEDED_NIPS=""
if [ -n "$FORCE_NIPS" ]; then
if [ "$FORCE_NIPS" = "all" ]; then
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 046 059 060 061"
print_info "Forced: Building all available NIPs"
else
# Convert comma-separated list to space-separated with 3-digit format
NEEDED_NIPS=$(echo "$FORCE_NIPS" | tr ',' ' ' | sed 's/\b\([0-9]\)\b/00\1/g' | sed 's/\b\([0-9][0-9]\)\b/0\1/g')
print_info "Forced NIPs: $NEEDED_NIPS"
fi
else
# Auto-detect from .c files in current directory
if ls *.c >/dev/null 2>&1; then
# Scan for nip*.h includes (with or without nostr_core/ prefix)
DETECTED=$(grep -h '#include[[:space:]]*["\<]\(nostr_core/\)\?nip[0-9][0-9][0-9]\.h["\>]' *.c 2>/dev/null | \
sed 's/.*nip0*\([0-9]*\)\.h.*/\1/' | \
sort -u | \
sed 's/\b\([0-9]\)\b/00\1/g' | sed 's/\b\([0-9][0-9]\)\b/0\1/g')
uninstall)
print_status "Uninstalling library from system..."
sudo make uninstall
print_success "Library uninstalled"
# Check for nostr_core.h (includes everything)
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
print_info "Found #include \"nostr_core.h\" - building all NIPs"
NEEDED_NIPS="001 004 005 006 011 013 019 042 044 046 059 060 061"
elif [ -n "$DETECTED" ]; then
NEEDED_NIPS="$DETECTED"
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
else
print_warning "No NIP includes detected in *.c files"
print_info "Defaulting to basic NIPs: 001, 006, 019"
NEEDED_NIPS="001 006 019"
fi
else
print_warning "No .c files found in current directory"
print_info "Defaulting to basic NIPs: 001, 006, 019"
NEEDED_NIPS="001 006 019"
fi
fi
# If building tests or examples, include all NIPs to ensure compatibility
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
print_info "Building tests/examples - including all available NIPs for compatibility"
fi
# Ensure NIP-001 is always included (required for core functionality)
if ! echo "$NEEDED_NIPS" | grep -q "001"; then
NEEDED_NIPS="001 $NEEDED_NIPS"
print_info "Added NIP-001 (required for core functionality)"
fi
###########################################################################################
###########################################################################################
############ AUTODETECT SYSTEM ARCHITECTURE
###########################################################################################
###########################################################################################
# Determine architecture
if [ -z "$ARCHITECTURE" ]; then
ARCH=$(uname -m)
case $ARCH in
x86_64|amd64)
ARCHITECTURE="x64"
;;
aarch64|arm64)
ARCHITECTURE="arm64"
;;
*)
ARCHITECTURE="x64" # Default fallback
print_warning "Unknown architecture '$ARCH', defaulting to x64"
;;
esac
fi
print_info "Target architecture: $ARCHITECTURE"
# Set compiler based on architecture
case $ARCHITECTURE in
x64)
CC="gcc"
ARCH_SUFFIX="x64"
;;
help|--help|-h)
show_usage
arm64)
CC="aarch64-linux-gnu-gcc"
ARCH_SUFFIX="arm64"
;;
*)
print_error "Unknown target: $TARGET"
echo ""
show_usage
print_error "Unsupported architecture: $ARCHITECTURE"
exit 1
;;
esac
# Check if compiler exists
if ! command -v $CC &> /dev/null; then
print_error "Compiler $CC not found"
if [ "$ARCHITECTURE" = "arm64" ]; then
print_info "Install ARM64 cross-compiler: sudo apt install gcc-aarch64-linux-gnu"
fi
exit 1
fi
###########################################################################################
###########################################################################################
############ CHECK AND BUILD DEPENDENCIES BASED ON NEEDED NIPS
###########################################################################################
###########################################################################################
print_info "Checking dependencies based on needed NIPs..."
# Determine which dependencies are needed based on NIPs
NEED_SECP256K1=false
NEED_OPENSSL=false
NEED_CURL=false
# secp256k1 is always needed (core cryptography for NIP-001)
NEED_SECP256K1=true
# Check if network-dependent NIPs are included
NETWORK_NIPS="005 011" # NIP-005 (DNS), NIP-011 (Relay info)
for nip in $NEEDED_NIPS; do
case $nip in
005|011)
NEED_CURL=true
print_info "NIP-$nip requires HTTP functionality - curl needed"
;;
esac
done
# WebSocket functionality is always enabled (using system OpenSSL)
NEED_OPENSSL=true
NEED_CURL=true
print_info "WebSocket functionality enabled - system OpenSSL and curl required"
if [ "$VERBOSE" = true ]; then
print_info "Dependency requirements:"
[ "$NEED_SECP256K1" = true ] && echo " ✓ secp256k1 (core crypto)"
[ "$NEED_OPENSSL" = true ] && echo " ✓ OpenSSL (TLS/WebSocket)"
[ "$NEED_CURL" = true ] && echo " ✓ curl (HTTP requests)"
fi
# Function to detect system secp256k1
detect_system_secp256k1() {
if [ "$NEED_SECP256K1" != true ]; then
return 0
fi
print_info "Detecting system secp256k1..."
# Try pkg-config first
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libsecp256k1; then
SECP256K1_CFLAGS=$(pkg-config --cflags libsecp256k1)
SECP256K1_LIBS=$(pkg-config --libs libsecp256k1)
if [ "$VERBOSE" = true ]; then
print_success "Found secp256k1 via pkg-config"
print_info " CFLAGS: $SECP256K1_CFLAGS"
print_info " LIBS: $SECP256K1_LIBS"
fi
return 0
fi
# Fallback to standard locations
SECP256K1_CFLAGS=""
SECP256K1_LIBS="-lsecp256k1"
# Check common header locations
for header_path in /usr/include/secp256k1.h /usr/local/include/secp256k1.h; do
if [ -f "$header_path" ]; then
if [ "$header_path" != "/usr/include/secp256k1.h" ]; then
SECP256K1_CFLAGS="-I$(dirname $header_path)"
fi
if [ "$VERBOSE" = true ]; then
print_success "Found secp256k1 headers at: $header_path"
fi
break
fi
done
# Check if we can find the libraries
if ! echo '#include <secp256k1.h>' | gcc $SECP256K1_CFLAGS -E - >/dev/null 2>&1; then
print_error "secp256k1 development headers not found"
print_info "Install with: sudo apt install libsecp256k1-dev (Ubuntu/Debian)"
print_info " sudo yum install libsecp256k1-devel (CentOS/RHEL)"
print_info " brew install secp256k1 (macOS)"
exit 1
fi
print_success "System secp256k1 detected"
}
# Function to detect system OpenSSL
detect_system_openssl() {
if [ "$NEED_OPENSSL" != true ]; then
return 0
fi
print_info "Detecting system OpenSSL..."
# Try pkg-config first
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists openssl; then
OPENSSL_CFLAGS=$(pkg-config --cflags openssl)
OPENSSL_LIBS=$(pkg-config --libs openssl)
if [ "$VERBOSE" = true ]; then
print_success "Found OpenSSL via pkg-config"
print_info " CFLAGS: $OPENSSL_CFLAGS"
print_info " LIBS: $OPENSSL_LIBS"
fi
return 0
fi
# Fallback to standard locations
OPENSSL_CFLAGS=""
OPENSSL_LIBS="-lssl -lcrypto"
# Check common header locations
for header_path in /usr/include/openssl /usr/local/include/openssl; do
if [ -f "$header_path/ssl.h" ]; then
if [ "$header_path" != "/usr/include/openssl" ]; then
OPENSSL_CFLAGS="-I$(dirname $header_path)"
fi
if [ "$VERBOSE" = true ]; then
print_success "Found OpenSSL headers at: $header_path"
fi
break
fi
done
# Check if we can find the libraries
if ! echo '#include <openssl/ssl.h>' | gcc $OPENSSL_CFLAGS -E - >/dev/null 2>&1; then
print_error "OpenSSL development headers not found"
print_info "Install with: sudo apt install libssl-dev (Ubuntu/Debian)"
print_info " sudo yum install openssl-devel (CentOS/RHEL)"
print_info " brew install openssl (macOS)"
exit 1
fi
print_success "System OpenSSL detected"
}
# Function to detect system curl
detect_system_curl() {
if [ "$NEED_CURL" != true ]; then
return 0
fi
print_info "Detecting system curl..."
# Try pkg-config first
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libcurl; then
CURL_CFLAGS=$(pkg-config --cflags libcurl)
CURL_LIBS=$(pkg-config --libs libcurl)
if [ "$VERBOSE" = true ]; then
print_success "Found curl via pkg-config"
print_info " CFLAGS: $CURL_CFLAGS"
print_info " LIBS: $CURL_LIBS"
fi
return 0
fi
# Fallback to standard locations
CURL_CFLAGS=""
CURL_LIBS="-lcurl"
# Check common header locations
for header_path in /usr/include/curl /usr/local/include/curl; do
if [ -f "$header_path/curl.h" ]; then
if [ "$header_path" != "/usr/include/curl" ]; then
CURL_CFLAGS="-I$(dirname $header_path)"
fi
if [ "$VERBOSE" = true ]; then
print_success "Found curl headers at: $header_path"
fi
break
fi
done
# Check if we can find the libraries
if ! echo '#include <curl/curl.h>' | gcc $CURL_CFLAGS -E - >/dev/null 2>&1; then
print_error "curl development headers not found"
print_info "Install with: sudo apt install libcurl4-openssl-dev (Ubuntu/Debian)"
print_info " sudo yum install libcurl-devel (CentOS/RHEL)"
print_info " brew install curl (macOS)"
exit 1
fi
print_success "System curl detected"
}
# Build only the needed dependencies
detect_system_secp256k1
detect_system_openssl
detect_system_curl
###########################################################################################
###########################################################################################
############ ADD CORE DEPENDENCIES THAT NEED TO BE BUILT TO THE $SOURCES VARIABLE
###########################################################################################
###########################################################################################
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 cjson/cJSON.c"
SOURCES="$SOURCES nostr_core/utils.c"
SOURCES="$SOURCES nostr_core/nostr_common.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"
NIP_DESCRIPTIONS=""
for nip in $NEEDED_NIPS; do
NIP_FILE="nostr_core/nip${nip}.c"
if [ -f "$NIP_FILE" ]; then
SOURCES="$SOURCES $NIP_FILE"
case $nip in
001) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-001(Basic)" ;;
004) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-004(Encrypt)" ;;
005) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-005(DNS)" ;;
006) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-006(Keys)" ;;
011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;;
013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;;
017) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-017(DMs)" ;;
019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;;
021) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-021(URI)" ;;
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
046) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-046(Remote-Signing)" ;;
059) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-059(Gift-Wrap)" ;;
060) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-060(Cashu-Wallet)" ;;
061) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-061(Nutzaps)" ;;
esac
else
print_warning "NIP file not found: $NIP_FILE - skipping"
fi
done
if echo "$NEEDED_NIPS" | grep -Eq '(^| )060( |$)|(^| )061( |$)'; then
SOURCES="$SOURCES nostr_core/cashu_mint.c"
fi
# Build flags
CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"
CFLAGS="$CFLAGS -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
INCLUDES="-I. -Inostr_core -Inostr_core/crypto -Icjson -Inostr_websocket"
# Add system library includes
if [ -n "$SECP256K1_CFLAGS" ]; then
INCLUDES="$INCLUDES $SECP256K1_CFLAGS"
fi
if [ -n "$OPENSSL_CFLAGS" ]; then
INCLUDES="$INCLUDES $OPENSSL_CFLAGS"
fi
if [ -n "$CURL_CFLAGS" ]; then
INCLUDES="$INCLUDES $CURL_CFLAGS"
fi
# System libraries
SYSTEM_LIBS=""
if [ -n "$SECP256K1_LIBS" ]; then
SYSTEM_LIBS="$SYSTEM_LIBS $SECP256K1_LIBS"
fi
if [ -n "$OPENSSL_LIBS" ]; then
SYSTEM_LIBS="$SYSTEM_LIBS $OPENSSL_LIBS"
fi
if [ -n "$CURL_LIBS" ]; then
SYSTEM_LIBS="$SYSTEM_LIBS $CURL_LIBS"
fi
# Output library name
OUTPUT="libnostr_core_${ARCH_SUFFIX}.a"
print_info "Compiling with: $CC"
print_info "Including:$NIP_DESCRIPTIONS"
if [ "$VERBOSE" = true ]; then
print_info "Sources: $SOURCES"
print_info "Flags: $CFLAGS $INCLUDES"
fi
###########################################################################################
###########################################################################################
############ COMPILE EACH SOURCE FROM $SOURCES INTO A .o FILE
###########################################################################################
###########################################################################################
OBJECTS=""
for source in $SOURCES; do
if [ -f "$source" ]; then
obj_name=$(basename "$source" .c).${ARCH_SUFFIX}.o
OBJECTS="$OBJECTS $obj_name"
if [ "$VERBOSE" = true ]; then
print_info "Compiling: $source -> $obj_name"
fi
#################################################
# THE ACTUAL COMMAND TO COMPILE .c FILES
#################################################
$CC $CFLAGS $INCLUDES -c "$source" -o "$obj_name"
if [ $? -ne 0 ]; then
print_error "Failed to compile $source"
exit 1
fi
else
print_error "Source file not found: $source"
exit 1
fi
done
###########################################################################################
###########################################################################################
############ CREATE THE FINAL STATIC LIBRARY FOR THE PROJECT: libnostr_core_XX.a
############ BY LINKING IN ALL OUR .o FILES THAT ARE REQUESTED TO BE ADDED.
###########################################################################################
###########################################################################################
print_info "Creating static library with system dependencies: $OUTPUT"
# Note: All crypto libraries (secp256k1, OpenSSL, curl) are now system dependencies
# Only our own object files are included in the static library
#########################################################
### THE ACTUAL COMMAND TO LINK .o FILES INTO A .a FILE
#########################################################
ar rcs "$OUTPUT" $OBJECTS
AR_RESULT=$?
###########################################################################################
###########################################################################################
############ IF THE LINKING OCCURED SUCCESSFULLY, BUILD THE TEST FILE EXECUTABLES
############ BY LINKING IN ALL OUR .o FILES THAT ARE REQUESTED TO BE ADDED.
###########################################################################################
###########################################################################################
if [ $AR_RESULT -eq 0 ]; then
# Cleanup object files
rm -f $OBJECTS
# Show library info
size_kb=$(du -k "$OUTPUT" | cut -f1)
print_success "Built $OUTPUT (${size_kb}KB) with:$NIP_DESCRIPTIONS"
# Build tests if requested
if [ "$BUILD_TESTS" = true ]; then
print_info "Scanning tests/ directory for test programs..."
if [ ! -d "tests" ]; then
print_warning "tests/ directory not found - skipping test builds"
else
TEST_COUNT=0
SUCCESS_COUNT=0
# Find all .c files in tests/ directory (not subdirectories)
while IFS= read -r -d '' test_file; do
TEST_COUNT=$((TEST_COUNT + 1))
test_name=$(basename "$test_file" .c)
test_exe="tests/$test_name"
print_info "Building test: $test_name"
# Test compilation with system libraries
LINK_FLAGS="-lz -ldl -lpthread -lm $SYSTEM_LIBS"
if [ "$VERBOSE" = true ]; then
print_info " Command: $CC $CFLAGS $INCLUDES \"$test_file\" -o \"$test_exe\" ./$OUTPUT $LINK_FLAGS"
fi
if $CC $CFLAGS $INCLUDES "$test_file" -o "$test_exe" "./$OUTPUT" $LINK_FLAGS; then
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
print_success "Built $test_name"
if [ "$VERBOSE" = true ]; then
print_info " Executable: $test_exe"
fi
else
print_error " Failed to build: $test_name"
fi
done < <(find tests/ -maxdepth 1 -name "*.c" -type f -print0)
if [ $TEST_COUNT -eq 0 ]; then
print_warning "No .c files found in tests/ directory"
else
print_success "Built $SUCCESS_COUNT/$TEST_COUNT test programs"
fi
fi
echo ""
fi
# Build examples if requested
if [ "$BUILD_EXAMPLES" = true ]; then
print_info "Scanning examples/ directory for example programs..."
if [ ! -d "examples" ]; then
print_warning "examples/ directory not found - skipping example builds"
else
EXAMPLE_COUNT=0
SUCCESS_COUNT=0
# Find all .c files in examples/ directory (not subdirectories)
while IFS= read -r -d '' example_file; do
EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1))
example_name=$(basename "$example_file" .c)
example_exe="examples/$example_name"
print_info "Building example: $example_name"
# Example compilation with system libraries
LINK_FLAGS="-lz -ldl -lpthread -lm $SYSTEM_LIBS"
if [ "$VERBOSE" = true ]; then
print_info " Command: $CC $CFLAGS $INCLUDES \"$example_file\" -o \"$example_exe\" ./$OUTPUT $LINK_FLAGS"
fi
if $CC $CFLAGS $INCLUDES "$example_file" -o "$example_exe" "./$OUTPUT" $LINK_FLAGS; then
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
print_success "Built $example_name"
if [ "$VERBOSE" = true ]; then
print_info " Executable: $example_exe"
fi
else
print_error " Failed to build: $example_name"
fi
done < <(find examples/ -maxdepth 1 -name "*.c" -type f -print0)
if [ $EXAMPLE_COUNT -eq 0 ]; then
print_warning "No .c files found in examples/ directory"
else
print_success "Built $SUCCESS_COUNT/$EXAMPLE_COUNT example programs"
fi
fi
echo ""
fi
echo "Usage in your project:"
echo " gcc your_app.c $OUTPUT -lz -ldl -lpthread -lm $SYSTEM_LIBS -o your_app"
echo ""
else
print_error "Failed to create static library"
exit 1
fi
+3191
View File
File diff suppressed because it is too large Load Diff
+306
View File
@@ -0,0 +1,306 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef cJSON__h
#define cJSON__h
#ifdef __cplusplus
extern "C"
{
#endif
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
#define __WINDOWS__
#endif
#ifdef __WINDOWS__
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
For *nix builds that support visibility attribute, you can define similar behavior by
setting default visibility to hidden by adding
-fvisibility=hidden (for gcc)
or
-xldscope=hidden (for sun cc)
to CFLAGS
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
*/
#define CJSON_CDECL __cdecl
#define CJSON_STDCALL __stdcall
/* export symbols by default, this is necessary for copy pasting the C and header file */
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_EXPORT_SYMBOLS
#endif
#if defined(CJSON_HIDE_SYMBOLS)
#define CJSON_PUBLIC(type) type CJSON_STDCALL
#elif defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
#elif defined(CJSON_IMPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
#endif
#else /* !__WINDOWS__ */
#define CJSON_CDECL
#define CJSON_STDCALL
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 18
#include <stddef.h>
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
/* The cJSON structure: */
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
typedef struct cJSON_Hooks
{
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;
typedef int cJSON_bool;
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_NESTING_LIMIT
#define CJSON_NESTING_LIMIT 1000
#endif
/* Limits the length of circular references can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_CIRCULAR_LIMIT
#define CJSON_CIRCULAR_LIMIT 10000
#endif
/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
/* These calls create a cJSON item of the appropriate type. */
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Create a string where valuestring references a string so
* it will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
/* Create an object/array that only references it's elements so
* they will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
/* These utilities create an Array of count items.
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
/* Append item to the specified array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
* writing to `item->string` */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
/* Remove/Detach items from Arrays/Objects. */
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
/* Update array items. */
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
/* Duplicate a cJSON item */
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
* The item->next and ->prev pointers are always zero on return from Duplicate. */
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
* The input pointer json cannot point to a read-only address area, such as a string constant,
* but should point to a readable and writable address area. */
CJSON_PUBLIC(void) cJSON_Minify(char *json);
/* Helper functions for creating and adding items to an object at the same time.
* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
/* helper for the cJSON_SetNumberValue macro */
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
#define cJSON_SetBoolValue(object, boolValue) ( \
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
cJSON_Invalid\
)
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif
#endif
-40
View File
@@ -1,40 +0,0 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# Find required dependencies
find_dependency(Threads REQUIRED)
# Include targets
include("${CMAKE_CURRENT_LIST_DIR}/nostr_core-targets.cmake")
# Set variables for backward compatibility
set(NOSTR_CORE_FOUND TRUE)
set(NOSTR_CORE_VERSION "@PROJECT_VERSION@")
# Check which libraries are available
set(NOSTR_CORE_STATIC_AVAILABLE FALSE)
set(NOSTR_CORE_SHARED_AVAILABLE FALSE)
if(TARGET nostr_core::static)
set(NOSTR_CORE_STATIC_AVAILABLE TRUE)
endif()
if(TARGET nostr_core::shared)
set(NOSTR_CORE_SHARED_AVAILABLE TRUE)
endif()
# Provide convenient variables
if(NOSTR_CORE_STATIC_AVAILABLE)
set(NOSTR_CORE_LIBRARIES nostr_core::static)
elseif(NOSTR_CORE_SHARED_AVAILABLE)
set(NOSTR_CORE_LIBRARIES nostr_core::shared)
endif()
set(NOSTR_CORE_INCLUDE_DIRS "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@/nostr")
# Feature information
set(NOSTR_CORE_ENABLE_WEBSOCKETS @NOSTR_ENABLE_WEBSOCKETS@)
set(NOSTR_CORE_USE_MBEDTLS @NOSTR_USE_MBEDTLS@)
check_required_components(nostr_core)
-107
View File
@@ -1,107 +0,0 @@
// Debug script to extract all intermediate values from nostr-tools NIP-44
const { v2 } = require('./nostr-tools/nip44.js');
const { bytesToHex, hexToBytes } = require('@noble/hashes/utils');
// Test vector 1: single char 'a'
console.log('=== NOSTR-TOOLS DEBUG: Single char "a" ===');
const sec1 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001');
const sec2 = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002');
const nonce = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001');
const plaintext = 'a';
// Step 1: Get public keys
const { schnorr } = require('@noble/curves/secp256k1');
const pub1 = bytesToHex(schnorr.getPublicKey(sec1));
const pub2 = bytesToHex(schnorr.getPublicKey(sec2));
console.log('pub1:', pub1);
console.log('pub2:', pub2);
// Step 2: Get conversation key
const conversationKey = v2.utils.getConversationKey(sec1, pub2);
console.log('conversation_key:', bytesToHex(conversationKey));
// Step 3: Get shared secret (raw ECDH)
const { secp256k1 } = require('@noble/curves/secp256k1');
const sharedPoint = secp256k1.getSharedSecret(sec1, '02' + pub2);
const sharedSecret = sharedPoint.subarray(1, 33); // X coordinate only
console.log('ecdh_shared_secret:', bytesToHex(sharedSecret));
// Step 4: Get message keys using internal function
const hkdf = require('@noble/hashes/hkdf');
const { sha256 } = require('@noble/hashes/sha256');
// HKDF Extract step
const salt = new TextEncoder().encode('nip44-v2');
const prk = hkdf.extract(sha256, sharedSecret, salt);
console.log('hkdf_extract_result:', bytesToHex(prk));
// HKDF Expand step
const messageKeys = hkdf.expand(sha256, prk, nonce, 76);
const chachaKey = messageKeys.subarray(0, 32);
const chachaNonce = messageKeys.subarray(32, 44);
const hmacKey = messageKeys.subarray(44, 76);
console.log('chacha_key:', bytesToHex(chachaKey));
console.log('chacha_nonce:', bytesToHex(chachaNonce));
console.log('hmac_key:', bytesToHex(hmacKey));
// Step 5: Pad the plaintext
function pad(plaintext) {
const utf8Encoder = new TextEncoder();
const unpadded = utf8Encoder.encode(plaintext);
const unpaddedLen = unpadded.length;
// Length prefix (big-endian u16)
const prefix = new Uint8Array(2);
new DataView(prefix.buffer).setUint16(0, unpaddedLen, false);
// Calculate padded length
function calcPaddedLen(len) {
if (len <= 32) return 32;
const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1);
const chunk = nextPower <= 256 ? 32 : nextPower / 8;
return chunk * (Math.floor((len - 1) / chunk) + 1);
}
const paddedLen = calcPaddedLen(unpaddedLen + 2);
const suffix = new Uint8Array(paddedLen - 2 - unpaddedLen);
// Combine: prefix + plaintext + padding
const result = new Uint8Array(paddedLen);
result.set(prefix);
result.set(unpadded, 2);
result.set(suffix, 2 + unpaddedLen);
return result;
}
const paddedPlaintext = pad(plaintext);
console.log('padded_plaintext:', bytesToHex(paddedPlaintext));
console.log('padded_length:', paddedPlaintext.length);
// Step 6: ChaCha20 encrypt
const { chacha20 } = require('@noble/ciphers/chacha');
const ciphertext = chacha20(chachaKey, chachaNonce, paddedPlaintext);
console.log('ciphertext:', bytesToHex(ciphertext));
// Step 7: HMAC with AAD
const { hmac } = require('@noble/hashes/hmac');
const { concatBytes } = require('@noble/hashes/utils');
const aad = concatBytes(nonce, ciphertext);
console.log('aad_data:', bytesToHex(aad));
const mac = hmac(sha256, hmacKey, aad);
console.log('mac:', bytesToHex(mac));
// Step 8: Final payload
const { base64 } = require('@scure/base');
const payload = concatBytes(new Uint8Array([2]), nonce, ciphertext, mac);
console.log('raw_payload:', bytesToHex(payload));
const base64Payload = base64.encode(payload);
console.log('final_payload:', base64Payload);
// Expected from test vectors
console.log('expected_payload:', 'AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb');
// Now let's also test the full encrypt function
const fullEncrypt = v2.encrypt(plaintext, conversationKey, nonce);
console.log('v2.encrypt_result:', fullEncrypt);
+121
View File
@@ -0,0 +1,121 @@
/*
* Example: NIP-60/NIP-61 Cashu Wallet Flow
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../nostr_core/nostr_core.h"
static void print_event(const char* title, cJSON* evt) {
if (!evt) {
printf("%s: <null>\n", title);
return;
}
char* s = cJSON_Print(evt);
if (s) {
printf("\n%s\n%s\n", title, s);
free(s);
}
}
int main(void) {
if (nostr_init() != NOSTR_SUCCESS) {
printf("Failed to initialize library\n");
return 1;
}
const char* user_sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char user_sk[32];
if (nostr_hex_to_bytes(user_sk_hex, user_sk, 32) != 0) {
printf("Invalid private key\n");
nostr_cleanup();
return 1;
}
/* 1) Create wallet event (kind:17375) */
char* mint_urls[] = {
"https://mint1.example.com",
"https://mint2.example.com"
};
nostr_nip60_wallet_data_t wallet_data;
memset(&wallet_data, 0, sizeof(wallet_data));
strcpy(wallet_data.privkey, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
wallet_data.mint_urls = mint_urls;
wallet_data.mint_count = 2;
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet_data, user_sk, 0);
print_event("Wallet Event (kind:17375)", wallet_event);
/* 2) Create token event (kind:7375) */
nostr_cashu_proof_t proofs[2];
memset(proofs, 0, sizeof(proofs));
strcpy(proofs[0].id, "005c2502034d4f12");
proofs[0].amount = 1;
proofs[0].secret = "secret-1";
proofs[0].C = "0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46";
strcpy(proofs[1].id, "005c2502034d4f12");
proofs[1].amount = 2;
proofs[1].secret = "secret-2";
proofs[1].C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
nostr_nip60_token_data_t token_data;
memset(&token_data, 0, sizeof(token_data));
token_data.mint_url = "https://mint1.example.com";
token_data.proofs = proofs;
token_data.proof_count = 2;
cJSON* token_event = nostr_nip60_create_token_event(&token_data, user_sk, 0);
print_event("Token Event (kind:7375)", token_event);
/* 3) Create spend history event (kind:7376) */
nostr_nip60_history_ref_t refs[1];
memset(refs, 0, sizeof(refs));
strcpy(refs[0].event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
refs[0].marker = NOSTR_NIP60_REF_CREATED;
nostr_nip60_history_data_t hist;
memset(&hist, 0, sizeof(hist));
hist.direction = NOSTR_NIP60_DIRECTION_IN;
hist.amount = 3;
hist.refs = refs;
hist.ref_count = 1;
cJSON* history_event = nostr_nip60_create_history_event(&hist, user_sk, 0);
print_event("History Event (kind:7376)", history_event);
/* 4) Create nutzap info event (kind:10019) */
char* relays[] = {"wss://relay1.example.com", "wss://relay2.example.com"};
char* mint_units[] = {"sat"};
nostr_nip61_mint_entry_t mint_entry;
memset(&mint_entry, 0, sizeof(mint_entry));
mint_entry.url = "https://mint1.example.com";
mint_entry.units = mint_units;
mint_entry.unit_count = 1;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
info.relay_urls = relays;
info.relay_count = 2;
info.mints = &mint_entry;
info.mint_count = 1;
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
cJSON* info_event = nostr_nip61_create_nutzap_info_event(&info, user_sk, 0);
print_event("Nutzap Info Event (kind:10019)", info_event);
/* 5) Optionally call Cashu mint HTTP endpoints with [cashu_mint_get_info()] */
printf("\nCashu mint integration is available via cashu_mint_* APIs.\n");
cJSON_Delete(info_event);
cJSON_Delete(history_event);
cJSON_Delete(token_event);
cJSON_Delete(wallet_event);
nostr_cleanup();
return 0;
}
@@ -1,39 +0,0 @@
# Example CMakeLists.txt for a project using nostr_core library
cmake_minimum_required(VERSION 3.12)
project(my_nostr_app VERSION 1.0.0 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
# Method 1: Find installed package
# Uncomment if nostr_core is installed system-wide
# find_package(nostr_core REQUIRED)
# Method 2: Use as subdirectory
# Uncomment if nostr_core is a subdirectory
# add_subdirectory(nostr_core)
# Method 3: Use pkg-config
# Uncomment if using pkg-config
# find_package(PkgConfig REQUIRED)
# pkg_check_modules(NOSTR_CORE REQUIRED nostr_core)
# Create executable
add_executable(my_nostr_app main.c)
# Link with nostr_core
# Choose one of the following based on your integration method:
# Method 1: Installed package
# target_link_libraries(my_nostr_app nostr_core::static)
# Method 2: Subdirectory
# target_link_libraries(my_nostr_app nostr_core_static)
# Method 3: pkg-config
# target_include_directories(my_nostr_app PRIVATE ${NOSTR_CORE_INCLUDE_DIRS})
# target_link_libraries(my_nostr_app ${NOSTR_CORE_LIBRARIES})
# For this example, we'll assume Method 2 (subdirectory)
# Add the parent nostr_core directory
add_subdirectory(../.. nostr_core)
target_link_libraries(my_nostr_app nostr_core_static)
-186
View File
@@ -1,186 +0,0 @@
# NOSTR Core Integration Example
This directory contains a complete example showing how to integrate the NOSTR Core library into your own projects.
## What This Example Demonstrates
- **Library Initialization**: Proper setup and cleanup of the NOSTR library
- **Identity Management**: Key generation, bech32 encoding, and format detection
- **Event Creation**: Creating and signing different types of NOSTR events
- **Input Handling**: Processing various input formats (mnemonic, hex, bech32)
- **Utility Functions**: Using helper functions for hex conversion and error handling
- **CMake Integration**: How to integrate the library in your CMake-based project
## Building and Running
### Method 1: Using CMake
```bash
# Create build directory
mkdir build && cd build
# Configure with CMake
cmake ..
# Build
make
# Run the example
./my_nostr_app
```
### Method 2: Manual Compilation
```bash
# Compile directly (assuming you're in the c_nostr root directory)
gcc -I. examples/integration_example/main.c nostr_core.c nostr_crypto.c cjson/cJSON.c -lm -o integration_example
# Run
./integration_example
```
## Expected Output
The example will demonstrate:
1. **Identity Management Demo**
- Generate a new keypair
- Display keys in hex and bech32 format
2. **Event Creation Demo**
- Create a text note event
- Create a profile event
- Display the JSON for both events
3. **Input Handling Demo**
- Process different input formats
- Show format detection and decoding
4. **Utility Functions Demo**
- Hex conversion round-trip
- Error message display
## Integration Patterns
### Pattern 1: CMake Find Package
If NOSTR Core is installed system-wide:
```cmake
find_package(nostr_core REQUIRED)
target_link_libraries(your_app nostr_core::static)
```
### Pattern 2: CMake Subdirectory
If NOSTR Core is a subdirectory of your project:
```cmake
add_subdirectory(nostr_core)
target_link_libraries(your_app nostr_core_static)
```
### Pattern 3: pkg-config
If using pkg-config:
```cmake
find_package(PkgConfig REQUIRED)
pkg_check_modules(NOSTR_CORE REQUIRED nostr_core)
target_include_directories(your_app PRIVATE ${NOSTR_CORE_INCLUDE_DIRS})
target_link_libraries(your_app ${NOSTR_CORE_LIBRARIES})
```
### Pattern 4: Direct Source Integration
Copy the essential files to your project:
```bash
cp nostr_core.{c,h} nostr_crypto.{c,h} your_project/src/
cp -r cjson/ your_project/src/
```
Then compile them with your project sources.
## Code Structure
### main.c Structure
The example is organized into clear demonstration functions:
- `demo_identity_management()` - Key generation and encoding
- `demo_event_creation()` - Creating different event types
- `demo_input_handling()` - Processing various input formats
- `demo_utilities()` - Using utility functions
Each function demonstrates specific aspects of the library while maintaining proper error handling and resource cleanup.
### Key Integration Points
1. **Initialization**
```c
int ret = nostr_init();
if (ret != NOSTR_SUCCESS) {
// Handle error
}
```
2. **Resource Cleanup**
```c
// Always clean up JSON objects
cJSON_Delete(event);
// Clean up library on exit
nostr_cleanup();
```
3. **Error Handling**
```c
if (ret != NOSTR_SUCCESS) {
printf("Error: %s\n", nostr_strerror(ret));
return ret;
}
```
## Customization
You can modify this example for your specific needs:
- Change the `app_config_t` structure to match your application's configuration
- Add additional event types or custom event creation logic
- Integrate with your existing error handling and logging systems
- Add networking functionality using the WebSocket layer
## Dependencies
This example requires:
- C99 compiler (gcc, clang)
- CMake 3.12+ (for CMake build)
- NOSTR Core library and its dependencies
## Testing
You can test different input formats by passing them as command line arguments:
```bash
# Test with mnemonic
./my_nostr_app "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
# Test with hex private key
./my_nostr_app "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
# Test with bech32 nsec
./my_nostr_app "nsec1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
## Next Steps
After studying this example, you can:
1. Integrate the patterns into your own application
2. Explore the WebSocket functionality for relay communication
3. Add support for additional NOSTR event types
4. Implement your own identity persistence layer
5. Add networking and relay management features
For more examples, see the other files in the `examples/` directory.
-271
View File
@@ -1,271 +0,0 @@
/*
* Example application demonstrating how to integrate nostr_core into other projects
* This shows a complete workflow from key generation to event publishing
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nostr_core.h"
// Example application configuration
typedef struct {
char* app_name;
char* version;
int debug_mode;
} app_config_t;
static app_config_t g_config = {
.app_name = "My NOSTR App",
.version = "1.0.0",
.debug_mode = 1
};
// Helper function to print hex data
static void print_hex(const char* label, const unsigned char* data, size_t len) {
if (g_config.debug_mode) {
printf("%s: ", label);
for (size_t i = 0; i < len; i++) {
printf("%02x", data[i]);
}
printf("\n");
}
}
// Helper function to print JSON nicely
static void print_event(const char* label, cJSON* event) {
if (!event) {
printf("%s: NULL\n", label);
return;
}
char* json_string = cJSON_Print(event);
if (json_string) {
printf("%s:\n%s\n", label, json_string);
free(json_string);
}
}
// Example: Generate and manage identity
static int demo_identity_management(void) {
printf("\n=== Identity Management Demo ===\n");
unsigned char private_key[32], public_key[32];
char nsec[100], npub[100];
// Generate a new keypair
printf("Generating new keypair...\n");
int ret = nostr_generate_keypair(private_key, public_key);
if (ret != NOSTR_SUCCESS) {
printf("Error generating keypair: %s\n", nostr_strerror(ret));
return ret;
}
print_hex("Private Key", private_key, 32);
print_hex("Public Key", public_key, 32);
// Convert to bech32 format
ret = nostr_key_to_bech32(private_key, "nsec", nsec);
if (ret != NOSTR_SUCCESS) {
printf("Error encoding nsec: %s\n", nostr_strerror(ret));
return ret;
}
ret = nostr_key_to_bech32(public_key, "npub", npub);
if (ret != NOSTR_SUCCESS) {
printf("Error encoding npub: %s\n", nostr_strerror(ret));
return ret;
}
printf("nsec: %s\n", nsec);
printf("npub: %s\n", npub);
return NOSTR_SUCCESS;
}
// Example: Create different types of events
static int demo_event_creation(const unsigned char* private_key) {
printf("\n=== Event Creation Demo ===\n");
// Create a text note
printf("Creating text note...\n");
cJSON* text_event = nostr_create_text_event("Hello from my NOSTR app!", private_key);
if (!text_event) {
printf("Error creating text event\n");
return NOSTR_ERROR_JSON_PARSE;
}
print_event("Text Event", text_event);
// Create a profile event
printf("\nCreating profile event...\n");
cJSON* profile_event = nostr_create_profile_event(
g_config.app_name,
"A sample application demonstrating NOSTR integration",
private_key
);
if (!profile_event) {
printf("Error creating profile event\n");
cJSON_Delete(text_event);
return NOSTR_ERROR_JSON_PARSE;
}
print_event("Profile Event", profile_event);
// Cleanup
cJSON_Delete(text_event);
cJSON_Delete(profile_event);
return NOSTR_SUCCESS;
}
// Example: Handle different input formats
static int demo_input_handling(const char* user_input) {
printf("\n=== Input Handling Demo ===\n");
printf("Processing input: %s\n", user_input);
// Detect input type
int input_type = nostr_detect_input_type(user_input);
switch (input_type) {
case NOSTR_INPUT_MNEMONIC:
printf("Detected: BIP39 Mnemonic\n");
{
unsigned char priv[32], pub[32];
int ret = nostr_derive_keys_from_mnemonic(user_input, 0, priv, pub);
if (ret == NOSTR_SUCCESS) {
print_hex("Derived Private Key", priv, 32);
print_hex("Derived Public Key", pub, 32);
}
}
break;
case NOSTR_INPUT_NSEC_HEX:
printf("Detected: Hex-encoded private key\n");
{
unsigned char decoded[32];
int ret = nostr_decode_nsec(user_input, decoded);
if (ret == NOSTR_SUCCESS) {
print_hex("Decoded Private Key", decoded, 32);
}
}
break;
case NOSTR_INPUT_NSEC_BECH32:
printf("Detected: Bech32-encoded private key (nsec)\n");
{
unsigned char decoded[32];
int ret = nostr_decode_nsec(user_input, decoded);
if (ret == NOSTR_SUCCESS) {
print_hex("Decoded Private Key", decoded, 32);
}
}
break;
default:
printf("Unknown input format\n");
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
// Example: Demonstrate utility functions
static int demo_utilities(void) {
printf("\n=== Utility Functions Demo ===\n");
// Hex conversion
const char* test_hex = "deadbeef";
unsigned char bytes[4];
char hex_result[9];
printf("Testing hex conversion with: %s\n", test_hex);
int ret = nostr_hex_to_bytes(test_hex, bytes, 4);
if (ret != NOSTR_SUCCESS) {
printf("Error in hex_to_bytes: %s\n", nostr_strerror(ret));
return ret;
}
nostr_bytes_to_hex(bytes, 4, hex_result);
printf("Round-trip result: %s\n", hex_result);
// Error message testing
printf("\nTesting error messages:\n");
for (int i = 0; i >= -10; i--) {
const char* msg = nostr_strerror(i);
if (msg && strlen(msg) > 0) {
printf(" %d: %s\n", i, msg);
}
}
return NOSTR_SUCCESS;
}
int main(int argc, char* argv[]) {
printf("%s v%s\n", g_config.app_name, g_config.version);
printf("NOSTR Core Integration Example\n");
printf("=====================================\n");
// Initialize the NOSTR library
printf("Initializing NOSTR core library...\n");
int ret = nostr_init();
if (ret != NOSTR_SUCCESS) {
printf("Failed to initialize NOSTR library: %s\n", nostr_strerror(ret));
return 1;
}
// Run demonstrations
unsigned char demo_private_key[32];
// 1. Identity management
ret = demo_identity_management();
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
// Generate a key for other demos
nostr_generate_keypair(demo_private_key, NULL);
// 2. Event creation
ret = demo_event_creation(demo_private_key);
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
// 3. Input handling (use command line argument if provided)
const char* test_input = (argc > 1) ? argv[1] :
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
ret = demo_input_handling(test_input);
if (ret != NOSTR_SUCCESS && ret != NOSTR_ERROR_INVALID_INPUT) {
goto cleanup;
}
// 4. Utility functions
ret = demo_utilities();
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
printf("\n=====================================\n");
printf("All demonstrations completed successfully!\n");
printf("\nThis example shows how to:\n");
printf(" • Initialize the NOSTR library\n");
printf(" • Generate and manage keypairs\n");
printf(" • Create and sign different event types\n");
printf(" • Handle various input formats\n");
printf(" • Use utility functions\n");
printf(" • Clean up resources properly\n");
ret = NOSTR_SUCCESS;
cleanup:
// Clean up the NOSTR library
printf("\nCleaning up NOSTR library...\n");
nostr_cleanup();
if (ret == NOSTR_SUCCESS) {
printf("Example completed successfully.\n");
return 0;
} else {
printf("Example failed with error: %s\n", nostr_strerror(ret));
return 1;
}
}
+125
View File
@@ -0,0 +1,125 @@
/*
* NIP-46 Remote Signer Example
*
* Demonstrates signer session setup, bunker URL generation,
* request parsing, request handling, and encrypted response event creation.
*/
#include "../nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int parse_hex_key(const char* hex, unsigned char out[32]) {
if (!hex || strlen(hex) != 64) return -1;
return nostr_hex_to_bytes(hex, out, 32);
}
int main(int argc, char** argv) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <signer_privkey_hex> <user_privkey_hex> [relay_url]\n", argv[0]);
fprintf(stderr, "Example: %s <64hex> <64hex> wss://relay.example.com\n", argv[0]);
return 1;
}
const char* relay = (argc >= 4) ? argv[3] : "wss://relay.example.com";
unsigned char signer_sk[32];
unsigned char user_sk[32];
if (parse_hex_key(argv[1], signer_sk) != 0 || parse_hex_key(argv[2], user_sk) != 0) {
fprintf(stderr, "Invalid private key hex; expected 64 hex characters for each key\n");
return 1;
}
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize nostr library\n");
return 1;
}
const char* relays[] = { relay };
nostr_nip46_signer_session_t signer;
int rc = nostr_nip46_signer_session_init(&signer, signer_sk, user_sk, relays, 1);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "signer init failed: %s\n", nostr_strerror(rc));
nostr_cleanup();
return 1;
}
char bunker_url[1024];
rc = nostr_nip46_signer_create_bunker_url(&signer, "demo-secret", bunker_url, sizeof(bunker_url));
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "failed to create bunker url: %s\n", nostr_strerror(rc));
nostr_nip46_signer_session_destroy(&signer);
nostr_cleanup();
return 1;
}
printf("NIP-46 signer initialized\n");
printf("Signer pubkey: %s\n", signer.signer_pubkey_hex);
printf("User pubkey: %s\n", signer.user_pubkey_hex);
printf("Relay: %s\n", relay);
printf("Bunker URL: %s\n\n", bunker_url);
// Simulate receiving a connect request and producing a response
const char* connect_params[] = { signer.signer_pubkey_hex, "demo-secret", "sign_event:1,get_public_key" };
nostr_nip46_request_t req;
rc = nostr_nip46_create_request("demo-req-1", NOSTR_NIP46_METHOD_CONNECT, connect_params, 3, &req);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "failed to create demo request: %s\n", nostr_strerror(rc));
nostr_nip46_signer_session_destroy(&signer);
nostr_cleanup();
return 1;
}
nostr_nip46_response_t resp;
rc = nostr_nip46_signer_handle_request(&signer, &req, &resp);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "failed to handle demo request: %s\n", nostr_strerror(rc));
nostr_nip46_free_request(&req);
nostr_nip46_signer_session_destroy(&signer);
nostr_cleanup();
return 1;
}
printf("Handled method: %s\n", req.method_str);
printf("Response result: %s\n", resp.result ? resp.result : "<none>");
printf("Response error: %s\n", resp.error ? resp.error : "<none>");
// Simulate building encrypted response event back to a client
unsigned char client_sk[32];
unsigned char client_pk[32];
memset(client_sk, 0, sizeof(client_sk));
client_sk[31] = 1; // deterministic demo key
if (nostr_ec_public_key_from_private_key(client_sk, client_pk) != 0) {
fprintf(stderr, "failed to derive demo client pubkey\n");
nostr_nip46_free_response(&resp);
nostr_nip46_free_request(&req);
nostr_nip46_signer_session_destroy(&signer);
nostr_cleanup();
return 1;
}
cJSON* response_event = nostr_nip46_create_response_event(&resp, signer.signer_private_key, client_pk, 0);
if (!response_event) {
fprintf(stderr, "failed to create encrypted response event\n");
nostr_nip46_free_response(&resp);
nostr_nip46_free_request(&req);
nostr_nip46_signer_session_destroy(&signer);
nostr_cleanup();
return 1;
}
char* response_event_json = cJSON_Print(response_event);
if (response_event_json) {
printf("\nEncrypted response event (publish this to relays):\n%s\n", response_event_json);
free(response_event_json);
}
cJSON_Delete(response_event);
nostr_nip46_free_response(&resp);
nostr_nip46_free_request(&req);
nostr_nip46_signer_session_destroy(&signer);
nostr_cleanup();
return 0;
}
+889
View File
@@ -0,0 +1,889 @@
/*
* Interactive Relay Pool Test Program
*
* Interactive command-line interface for testing nostr_relay_pool functionality.
* All output is logged to pool.log while the menu runs in the terminal.
*
* Usage: ./pool_test
*/
#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
// Global variables
volatile sig_atomic_t running = 1;
nostr_relay_pool_t* pool = NULL;
nostr_pool_subscription_t** subscriptions = NULL;
int subscription_count = 0;
int subscription_capacity = 0;
pthread_t poll_thread;
int log_fd = -1;
// Signal handler for clean shutdown
void signal_handler(int signum) {
(void)signum;
running = 0;
}
// Event callback - called when an event is received
void on_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data;
// Extract basic event information
cJSON* id = cJSON_GetObjectItem(event, "id");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
cJSON* kind = cJSON_GetObjectItem(event, "kind");
cJSON* content = cJSON_GetObjectItem(event, "content");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0'; // Remove newline
dprintf(log_fd, "[%s] 📨 EVENT from %s\n", timestamp, relay_url);
dprintf(log_fd, "├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
dprintf(log_fd, "├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
dprintf(log_fd, "├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
dprintf(log_fd, "├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
// Truncate content if too long
if (content && cJSON_IsString(content)) {
const char* content_str = cJSON_GetStringValue(content);
size_t content_len = strlen(content_str);
if (content_len > 100) {
dprintf(log_fd, "└── Content: %.97s...\n", content_str);
} else {
dprintf(log_fd, "└── Content: %s\n", content_str);
}
} else {
dprintf(log_fd, "└── Content: (empty)\n");
}
dprintf(log_fd, "\n");
}
// EOSE callback - called when End of Stored Events is received
void on_eose(cJSON** events, int event_count, void* user_data) {
(void)user_data;
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 📋 EOSE received - %d events collected\n", timestamp, event_count);
// Log collected events if any
for (int i = 0; i < event_count; i++) {
cJSON* id = cJSON_GetObjectItem(events[i], "id");
if (id && cJSON_IsString(id)) {
dprintf(log_fd, " Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
}
}
dprintf(log_fd, "\n");
}
// Background polling thread
void* poll_thread_func(void* arg) {
(void)arg;
while (running) {
if (pool) {
nostr_relay_pool_poll(pool, 100);
}
struct timespec ts = {0, 10000000}; // 10ms
nanosleep(&ts, NULL);
}
return NULL;
}
// Print menu
void print_menu() {
printf("\n=== NOSTR Relay Pool Test Menu ===\n");
printf("1. Start Pool (ws://localhost:7555)\n");
printf("2. Stop Pool\n");
printf("3. Add relay to pool\n");
printf("4. Remove relay from pool\n");
printf("5. Add subscription\n");
printf("6. Remove subscription\n");
printf("7. Show pool status\n");
printf("8. Test reconnection (simulate disconnect)\n");
printf("9. Publish Event\n");
printf("0. Exit\n");
printf("Choice: ");
}
// Get user input with default
char* get_input(const char* prompt, const char* default_value) {
static char buffer[1024];
printf("%s", prompt);
if (default_value) {
printf(" [%s]", default_value);
}
printf(": ");
if (!fgets(buffer, sizeof(buffer), stdin)) {
return NULL;
}
// Remove newline
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
// Return default if empty
if (strlen(buffer) == 0 && default_value) {
return strdup(default_value);
}
return strdup(buffer);
}
// Parse comma-separated list into cJSON array
cJSON* parse_comma_list(const char* input, int is_number) {
if (!input || strlen(input) == 0) {
return NULL;
}
cJSON* array = cJSON_CreateArray();
if (!array) return NULL;
char* input_copy = strdup(input);
char* token = strtok(input_copy, ",");
while (token) {
// Trim whitespace
while (*token == ' ') token++;
char* end = token + strlen(token) - 1;
while (end > token && *end == ' ') *end-- = '\0';
if (is_number) {
int num = atoi(token);
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
} else {
cJSON_AddItemToArray(array, cJSON_CreateString(token));
}
token = strtok(NULL, ",");
}
free(input_copy);
return array;
}
// Add subscription interactively
void add_subscription() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
printf("\n--- Add Subscription ---\n");
printf("Enter filter values (press Enter for no value):\n");
cJSON* filter = cJSON_CreateObject();
// ids
char* ids_input = get_input("ids (comma-separated event ids)", NULL);
if (ids_input && strlen(ids_input) > 0) {
cJSON* ids = parse_comma_list(ids_input, 0);
if (ids) cJSON_AddItemToObject(filter, "ids", ids);
}
free(ids_input);
// authors
char* authors_input = get_input("authors (comma-separated pubkeys)", NULL);
if (authors_input && strlen(authors_input) > 0) {
cJSON* authors = parse_comma_list(authors_input, 0);
if (authors) cJSON_AddItemToObject(filter, "authors", authors);
}
free(authors_input);
// kinds
char* kinds_input = get_input("kinds (comma-separated numbers)", NULL);
if (kinds_input && strlen(kinds_input) > 0) {
cJSON* kinds = parse_comma_list(kinds_input, 1);
if (kinds) cJSON_AddItemToObject(filter, "kinds", kinds);
}
free(kinds_input);
// #e tag
char* e_input = get_input("#e (comma-separated event ids)", NULL);
if (e_input && strlen(e_input) > 0) {
cJSON* e_array = parse_comma_list(e_input, 0);
if (e_array) cJSON_AddItemToObject(filter, "#e", e_array);
}
free(e_input);
// #p tag
char* p_input = get_input("#p (comma-separated pubkeys)", NULL);
if (p_input && strlen(p_input) > 0) {
cJSON* p_array = parse_comma_list(p_input, 0);
if (p_array) cJSON_AddItemToObject(filter, "#p", p_array);
}
free(p_input);
// since
char* since_input = get_input("since (unix timestamp or 'n' for now)", NULL);
if (since_input && strlen(since_input) > 0) {
if (strcmp(since_input, "n") == 0) {
// Use current timestamp
time_t now = time(NULL);
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber((int)now));
printf("Using current timestamp: %ld\n", now);
} else {
int since = atoi(since_input);
if (since > 0) cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
}
}
free(since_input);
// until
char* until_input = get_input("until (unix timestamp)", NULL);
if (until_input && strlen(until_input) > 0) {
int until = atoi(until_input);
if (until > 0) cJSON_AddItemToObject(filter, "until", cJSON_CreateNumber(until));
}
free(until_input);
// limit
char* limit_input = get_input("limit (max events)", "10");
if (limit_input && strlen(limit_input) > 0) {
int limit = atoi(limit_input);
if (limit > 0) cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(limit));
}
free(limit_input);
// Get relay URLs from pool
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
cJSON_Delete(filter);
return;
}
// Ask about close_on_eose behavior
char* close_input = get_input("Close subscription on EOSE? (y/n)", "n");
int close_on_eose = (close_input && strcmp(close_input, "y") == 0) ? 1 : 0;
free(close_input);
// Create subscription with new parameters
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool,
(const char**)relay_urls,
relay_count,
filter,
on_event,
on_eose,
NULL,
close_on_eose,
1, // enable_deduplication
NOSTR_POOL_EOSE_FULL_SET, // result_mode
30, // relay_timeout_seconds
60 // eose_timeout_seconds
);
// Free relay URLs
for (int i = 0; i < relay_count; i++) {
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
if (!sub) {
printf("❌ Failed to create subscription\n");
cJSON_Delete(filter);
return;
}
// Store subscription
if (subscription_count >= subscription_capacity) {
subscription_capacity = subscription_capacity == 0 ? 10 : subscription_capacity * 2;
subscriptions = realloc(subscriptions, subscription_capacity * sizeof(nostr_pool_subscription_t*));
}
subscriptions[subscription_count++] = sub;
printf("✅ Subscription created (ID: %d)\n", subscription_count);
// Log the filter
char* filter_json = cJSON_Print(filter);
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🔍 New subscription created (ID: %d)\n", timestamp, subscription_count);
dprintf(log_fd, "Filter: %s\n\n", filter_json);
free(filter_json);
}
// Remove subscription
void remove_subscription() {
if (subscription_count == 0) {
printf("❌ No subscriptions to remove\n");
return;
}
printf("\n--- Remove Subscription ---\n");
printf("Available subscriptions:\n");
for (int i = 0; i < subscription_count; i++) {
printf("%d. Subscription %d\n", i + 1, i + 1);
}
char* choice_input = get_input("Enter subscription number to remove", NULL);
if (!choice_input || strlen(choice_input) == 0) {
free(choice_input);
return;
}
int choice = atoi(choice_input) - 1;
free(choice_input);
if (choice < 0 || choice >= subscription_count) {
printf("❌ Invalid subscription number\n");
return;
}
nostr_pool_subscription_close(subscriptions[choice]);
// Shift remaining subscriptions
for (int i = choice; i < subscription_count - 1; i++) {
subscriptions[i] = subscriptions[i + 1];
}
subscription_count--;
printf("✅ Subscription removed\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🗑️ Subscription removed (was ID: %d)\n\n", timestamp, choice + 1);
}
// Show pool status
void show_pool_status() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
// Give polling thread time to establish connections
printf("⏳ Waiting for connections to establish...\n");
sleep(3);
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
printf("\n📊 POOL STATUS\n");
printf("Relays: %d\n", relay_count);
printf("Subscriptions: %d\n", subscription_count);
if (relay_count > 0) {
printf("\nRelay Details:\n");
for (int i = 0; i < relay_count; i++) {
const char* status_str;
switch (statuses[i]) {
case NOSTR_POOL_RELAY_CONNECTED: status_str = "🟢 CONNECTED"; break;
case NOSTR_POOL_RELAY_CONNECTING: status_str = "🟡 CONNECTING"; break;
case NOSTR_POOL_RELAY_DISCONNECTED: status_str = "⚪ DISCONNECTED"; break;
case NOSTR_POOL_RELAY_ERROR: status_str = "🔴 ERROR"; break;
default: status_str = "❓ UNKNOWN"; break;
}
printf("├── %s: %s\n", relay_urls[i], status_str);
// Show connection and publish error details
const char* conn_error = nostr_relay_pool_get_relay_last_connection_error(pool, relay_urls[i]);
const char* pub_error = nostr_relay_pool_get_relay_last_publish_error(pool, relay_urls[i]);
if (conn_error) {
printf("│ ├── Connection error: %s\n", conn_error);
}
if (pub_error) {
printf("│ ├── Last publish error: %s\n", pub_error);
}
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
if (stats) {
printf("│ ├── Events received: %d\n", stats->events_received);
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
printf("│ ├── Connection failures: %d\n", stats->connection_failures);
printf("│ ├── Events published: %d (OK: %d, Failed: %d)\n",
stats->events_published, stats->events_published_ok, stats->events_published_failed);
printf("│ ├── Ping latency: %.2f ms\n", stats->ping_latency_current);
printf("│ └── Query latency: %.2f ms\n", stats->query_latency_avg);
}
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
}
printf("\n");
}
// Async publish callback context
typedef struct {
int total_relays;
int responses_received;
int success_count;
time_t start_time;
} async_publish_context_t;
// Async publish callback - called for each relay response
void async_publish_callback(const char* relay_url, const char* event_id,
int success, const char* message, void* user_data) {
async_publish_context_t* ctx = (async_publish_context_t*)user_data;
ctx->responses_received++;
if (success) {
ctx->success_count++;
}
// Calculate elapsed time
time_t now = time(NULL);
double elapsed = difftime(now, ctx->start_time);
// Log to file with real-time feedback
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
if (success) {
printf("✅ %s: Published successfully (%.1fs)\n", relay_url, elapsed);
dprintf(log_fd, "[%s] ✅ ASYNC: %s published successfully (%.1fs)\n",
timestamp, relay_url, elapsed);
} else {
printf("❌ %s: Failed - %s (%.1fs)\n", relay_url, message ? message : "unknown error", elapsed);
dprintf(log_fd, "[%s] ❌ ASYNC: %s failed - %s (%.1fs)\n",
timestamp, relay_url, message ? message : "unknown error", elapsed);
}
// Show progress
printf(" Progress: %d/%d responses received\n", ctx->responses_received, ctx->total_relays);
if (ctx->responses_received >= ctx->total_relays) {
printf("\n🎉 All relays responded! Final result: %d/%d successful\n",
ctx->success_count, ctx->total_relays);
dprintf(log_fd, "[%s] 🎉 ASYNC: All relays responded - %d/%d successful\n\n",
timestamp, ctx->success_count, ctx->total_relays);
}
}
// Publish test event with async callbacks
void publish_event() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
printf("\n--- Publish Test Event ---\n");
// Generate random keypair
unsigned char private_key[32], public_key[32];
if (nostr_generate_keypair(private_key, public_key) != NOSTR_SUCCESS) {
printf("❌ Failed to generate keypair\n");
return;
}
// Get current timestamp
time_t now = time(NULL);
// Format content with date/time
char content[256];
struct tm* tm_info = localtime(&now);
strftime(content, sizeof(content), "Test post at %Y-%m-%d %H:%M:%S", tm_info);
// Create kind 1 event
cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, now);
if (!event) {
printf("❌ Failed to create event\n");
return;
}
// Get relay URLs from pool
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
cJSON_Delete(event);
return;
}
printf("📤 Publishing event to %d relay(s)...\n", relay_count);
printf("Watch for real-time responses below:\n\n");
// Setup callback context
async_publish_context_t ctx = {0};
ctx.total_relays = relay_count;
ctx.start_time = time(NULL);
// Log the event
char* event_json = cJSON_Print(event);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 📤 Publishing test event\n", timestamp);
dprintf(log_fd, "Event: %s\n\n", event_json);
free(event_json);
// Publish using async function
int sent_count = nostr_relay_pool_publish_async(pool, (const char**)relay_urls,
relay_count, event,
async_publish_callback, &ctx);
if (sent_count > 0) {
printf("📡 Event sent to %d/%d relays, waiting for responses...\n\n",
sent_count, relay_count);
// Wait for all responses or timeout (10 seconds)
time_t wait_start = time(NULL);
while (ctx.responses_received < ctx.total_relays &&
(time(NULL) - wait_start) < 10) {
// Let the polling thread process messages
usleep(100000); // 100ms
}
if (ctx.responses_received < ctx.total_relays) {
printf("\n⏰ Timeout reached - %d/%d relays responded\n",
ctx.responses_received, ctx.total_relays);
}
} else {
printf("❌ Failed to send event to any relays\n");
}
// Cleanup
for (int i = 0; i < relay_count; i++) {
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
cJSON_Delete(event);
}
int main() {
// Setup logging to file
log_fd = open("pool.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (log_fd == -1) {
fprintf(stderr, "❌ Failed to open pool.log for writing\n");
return 1;
}
// Initialize NOSTR library
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
close(log_fd);
return 1;
}
// Setup signal handler
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Start polling thread
if (pthread_create(&poll_thread, NULL, poll_thread_func, NULL) != 0) {
fprintf(stderr, "❌ Failed to create polling thread\n");
nostr_cleanup();
close(log_fd);
return 1;
}
printf("🔗 NOSTR Relay Pool Interactive Test\n");
printf("=====================================\n");
printf("All event output is logged to pool.log\n");
printf("Press Ctrl+C to exit\n\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🚀 Pool test started\n\n", timestamp);
// Main menu loop
while (running) {
print_menu();
char choice;
if (scanf("%c", &choice) != 1) {
break;
}
// Consume newline
int c;
while ((c = getchar()) != '\n' && c != EOF);
switch (choice) {
case '1': { // Start Pool
if (pool) {
printf("❌ Pool already started\n");
break;
}
// Create pool with custom reconnection configuration for faster testing
nostr_pool_reconnect_config_t config = *nostr_pool_reconnect_config_default();
config.ping_interval_seconds = 5; // Ping every 5 seconds for testing
pool = nostr_relay_pool_create(&config);
if (!pool) {
printf("❌ Failed to create pool\n");
break;
}
if (nostr_relay_pool_add_relay(pool, "ws://localhost:7555") != NOSTR_SUCCESS) {
printf("❌ Failed to add default relay\n");
nostr_relay_pool_destroy(pool);
pool = NULL;
break;
}
printf("✅ Pool started with ws://localhost:7555\n");
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🏊 Pool started with default relay\n\n", timestamp);
break;
}
case '2': { // Stop Pool
if (!pool) {
printf("❌ Pool not started\n");
break;
}
// Close all subscriptions
for (int i = 0; i < subscription_count; i++) {
if (subscriptions[i]) {
nostr_pool_subscription_close(subscriptions[i]);
}
}
free(subscriptions);
subscriptions = NULL;
subscription_count = 0;
subscription_capacity = 0;
nostr_relay_pool_destroy(pool);
pool = NULL;
printf("✅ Pool stopped\n");
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🛑 Pool stopped\n\n", timestamp);
break;
}
case '3': { // Add relay
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char* url = get_input("Enter relay URL", "wss://relay.example.com");
if (url && strlen(url) > 0) {
if (nostr_relay_pool_add_relay(pool, url) == NOSTR_SUCCESS) {
printf("✅ Relay added: %s\n", url);
printf("⏳ Attempting to connect...\n");
// Give it a moment to attempt connection
sleep(2);
// Check connection status and show any errors
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(pool, url);
const char* error_msg = nostr_relay_pool_get_relay_last_connection_error(pool, url);
switch (status) {
case NOSTR_POOL_RELAY_CONNECTED:
printf("🟢 Successfully connected to %s\n", url);
break;
case NOSTR_POOL_RELAY_CONNECTING:
printf("🟡 Still connecting to %s...\n", url);
break;
case NOSTR_POOL_RELAY_DISCONNECTED:
printf("⚪ Disconnected from %s\n", url);
if (error_msg) {
printf(" Last error: %s\n", error_msg);
}
break;
case NOSTR_POOL_RELAY_ERROR:
printf("🔴 Connection error for %s\n", url);
if (error_msg) {
printf(" Error details: %s\n", error_msg);
}
break;
default:
printf("❓ Unknown status for %s\n", url);
break;
}
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] Relay added: %s (status: %d)\n", timestamp, url, status);
if (error_msg) {
dprintf(log_fd, " Connection error: %s\n", error_msg);
}
dprintf(log_fd, "\n");
} else {
printf("❌ Failed to add relay to pool\n");
}
}
free(url);
break;
}
case '4': { // Remove relay
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char* url = get_input("Enter relay URL to remove", NULL);
if (url && strlen(url) > 0) {
if (nostr_relay_pool_remove_relay(pool, url) == NOSTR_SUCCESS) {
printf("✅ Relay removed: %s\n", url);
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] Relay removed: %s\n\n", timestamp, url);
} else {
printf("❌ Failed to remove relay\n");
}
}
free(url);
break;
}
case '5': // Add subscription
add_subscription();
break;
case '6': // Remove subscription
remove_subscription();
break;
case '7': // Show status
show_pool_status();
break;
case '8': { // Test reconnection
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
break;
}
printf("\n--- Test Reconnection ---\n");
printf("Available relays:\n");
for (int i = 0; i < relay_count; i++) {
printf("%d. %s (%s)\n", i + 1, relay_urls[i],
statuses[i] == NOSTR_POOL_RELAY_CONNECTED ? "CONNECTED" : "NOT CONNECTED");
}
char* choice_input = get_input("Enter relay number to test reconnection with", NULL);
if (!choice_input || strlen(choice_input) == 0) {
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
free(choice_input);
break;
}
int choice = atoi(choice_input) - 1;
free(choice_input);
if (choice < 0 || choice >= relay_count) {
printf("❌ Invalid relay number\n");
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
break;
}
printf("🔄 Testing reconnection with %s...\n", relay_urls[choice]);
printf(" The pool is configured with automatic reconnection enabled.\n");
printf(" If the connection drops, it will automatically attempt to reconnect\n");
printf(" with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s max).\n");
printf(" Connection health is monitored with ping/pong every 30 seconds.\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🔄 TEST: Testing reconnection behavior with %s\n", timestamp, relay_urls[choice]);
dprintf(log_fd, " Pool configured with: auto-reconnect=ON, max_attempts=10, ping_interval=30s\n\n");
printf("✅ Reconnection test initiated. Monitor the status and logs for reconnection activity.\n");
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
break;
}
case '9': // Publish Event
publish_event();
break;
case '0': // Exit
running = 0;
break;
default:
printf("❌ Invalid choice\n");
break;
}
}
printf("\n🧹 Cleaning up...\n");
// Stop polling thread
running = 0;
pthread_join(poll_thread, NULL);
// Clean up pool and subscriptions
if (pool) {
for (int i = 0; i < subscription_count; i++) {
if (subscriptions[i]) {
nostr_pool_subscription_close(subscriptions[i]);
}
}
free(subscriptions);
nostr_relay_pool_destroy(pool);
printf("✅ Pool destroyed\n");
}
// Cleanup
nostr_cleanup();
close(log_fd);
printf("👋 Test completed\n");
return 0;
}
+242
View File
@@ -0,0 +1,242 @@
/*
* NIP-17 Private Direct Messages - Command Line Application
*
* This example demonstrates how to send NIP-17 private direct messages
* using the Nostr Core Library.
*
* Usage:
* ./send_nip17_dm <recipient_pubkey> <message> [sender_nsec]
*
* Arguments:
* recipient_pubkey: The npub or hex public key of the recipient
* message: The message to send
* sender_nsec: (optional) The nsec private key to use for sending.
* If not provided, uses a default test key.
*
* Example:
* ./send_nip17_dm npub1example... "Hello from NIP-17!" nsec1test...
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Default test private key (for demonstration - DO NOT USE IN PRODUCTION)
#define DEFAULT_SENDER_NSEC "nsec12kgt0dv2k2safv6s32w8f89z9uw27e68hjaa0d66c5xvk70ezpwqncd045"
// Default relay for sending DMs
#define DEFAULT_RELAY "wss://relay.laantungir.net"
// Progress callback for publishing
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);
if (message) {
printf(" - %s", message);
}
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
} else {
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
}
}
/**
* Convert npub to hex if needed
*/
int convert_pubkey_to_hex(const char* input_pubkey, char* output_hex) {
// Check if it's already hex (64 characters)
if (strlen(input_pubkey) == 64) {
// Assume it's already hex
strcpy(output_hex, input_pubkey);
return 0;
}
// Check if it's an npub (starts with "npub1")
if (strncmp(input_pubkey, "npub1", 5) == 0) {
// Convert npub to hex
unsigned char pubkey_bytes[32];
if (nostr_decode_npub(input_pubkey, pubkey_bytes) != 0) {
fprintf(stderr, "Error: Invalid npub format\n");
return -1;
}
nostr_bytes_to_hex(pubkey_bytes, 32, output_hex);
return 0;
}
fprintf(stderr, "Error: Public key must be 64-character hex or valid npub\n");
return -1;
}
/**
* Convert nsec to private key bytes if needed
*/
int convert_nsec_to_private_key(const char* input_nsec, unsigned char* private_key) {
// Check if it's already hex (64 characters)
if (strlen(input_nsec) == 64) {
// Convert hex to bytes
if (nostr_hex_to_bytes(input_nsec, private_key, 32) != 0) {
fprintf(stderr, "Error: Invalid hex private key\n");
return -1;
}
return 0;
}
// Check if it's an nsec (starts with "nsec1")
if (strncmp(input_nsec, "nsec1", 5) == 0) {
// Convert nsec directly to private key bytes
if (nostr_decode_nsec(input_nsec, private_key) != 0) {
fprintf(stderr, "Error: Invalid nsec format\n");
return -1;
}
return 0;
}
fprintf(stderr, "Error: Private key must be 64-character hex or valid nsec\n");
return -1;
}
/**
* Main function
*/
int main(int argc, char* argv[]) {
if (argc < 3 || argc > 4) {
fprintf(stderr, "Usage: %s <recipient_pubkey> <message> [sender_nsec]\n\n", argv[0]);
fprintf(stderr, "Arguments:\n");
fprintf(stderr, " recipient_pubkey: npub or hex public key of recipient\n");
fprintf(stderr, " message: The message to send\n");
fprintf(stderr, " sender_nsec: (optional) nsec private key. Uses test key if not provided.\n\n");
fprintf(stderr, "Example:\n");
fprintf(stderr, " %s npub1example... \"Hello!\" nsec1test...\n", argv[0]);
return 1;
}
const char* recipient_pubkey_input = argv[1];
const char* message = argv[2];
const char* sender_nsec_input = (argc >= 4) ? argv[3] : DEFAULT_SENDER_NSEC;
printf("🧪 NIP-17 Private Direct Message Sender\n");
printf("======================================\n\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Convert recipient pubkey
char recipient_pubkey_hex[65];
if (convert_pubkey_to_hex(recipient_pubkey_input, recipient_pubkey_hex) != 0) {
return 1;
}
// Convert sender private key
unsigned char sender_privkey[32];
if (convert_nsec_to_private_key(sender_nsec_input, sender_privkey) != 0) {
return 1;
}
// Derive sender public key for display
unsigned char sender_pubkey_bytes[32];
char sender_pubkey_hex[65];
if (nostr_ec_public_key_from_private_key(sender_privkey, sender_pubkey_bytes) != 0) {
fprintf(stderr, "Failed to derive sender public key\n");
return 1;
}
nostr_bytes_to_hex(sender_pubkey_bytes, 32, sender_pubkey_hex);
printf("📤 Sender: %s\n", sender_pubkey_hex);
printf("📥 Recipient: %s\n", recipient_pubkey_hex);
printf("💬 Message: %s\n", message);
printf("🌐 Relay: %s\n\n", DEFAULT_RELAY);
// Create DM event
printf("💬 Creating DM event...\n");
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
cJSON* dm_event = nostr_nip17_create_chat_event(
message,
recipient_pubkeys,
1,
"NIP-17 CLI", // subject
NULL, // no reply
DEFAULT_RELAY, // relay hint
sender_pubkey_hex
);
if (!dm_event) {
fprintf(stderr, "Failed to create DM event\n");
return 1;
}
printf("✅ Created DM event (kind 14)\n");
// Send DM (create gift wraps)
printf("🎁 Creating gift wraps...\n");
cJSON* gift_wraps[10]; // Max 10 gift wraps
int gift_wrap_count = nostr_nip17_send_dm(
dm_event,
recipient_pubkeys,
1,
sender_privkey,
gift_wraps,
10
);
cJSON_Delete(dm_event); // Original DM event no longer needed
if (gift_wrap_count <= 0) {
fprintf(stderr, "Failed to create gift wraps\n");
return 1;
}
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
// Publish the gift wrap to relay
printf("\n📤 Publishing gift wrap to relay...\n");
const char* relay_urls[] = {DEFAULT_RELAY};
int success_count = 0;
publish_result_t* publish_results = synchronous_publish_event_with_progress(
relay_urls,
1, // single relay
gift_wraps[0], // Send the first gift wrap
&success_count,
10, // 10 second timeout
publish_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
if (!publish_results || success_count != 1) {
fprintf(stderr, "\n❌ Failed to publish gift wrap (success_count: %d)\n", success_count);
// Clean up gift wraps
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
if (publish_results) free(publish_results);
return 1;
}
printf("\n✅ Successfully published NIP-17 DM!\n");
// Clean up
free(publish_results);
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
nostr_cleanup();
printf("\n🎉 DM sent successfully! The recipient can now decrypt it using their private key.\n");
return 0;
}
+63
View File
@@ -0,0 +1,63 @@
/*
* NOSTR Core Library - Version Test Example
* Simple version display using VERSION file
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nostr_core.h"
// Simple function to read VERSION file if it exists
static const char* get_version_from_file(void) {
static char version_buf[64] = {0};
FILE* f = fopen("VERSION", "r");
if (f) {
if (fgets(version_buf, sizeof(version_buf), f)) {
// Remove trailing newline
size_t len = strlen(version_buf);
if (len > 0 && version_buf[len-1] == '\n') {
version_buf[len-1] = '\0';
}
}
fclose(f);
return version_buf;
}
return "unknown";
}
int main() {
printf("NOSTR Core Library Version Test\n");
printf("===============================\n\n");
// Initialize the library
if (nostr_init() != NOSTR_SUCCESS) {
printf("Failed to initialize NOSTR library\n");
return 1;
}
// Display basic version information
const char* version = get_version_from_file();
printf("Library Version: %s\n", version);
printf("Library Status: Initialized successfully\n");
// Test basic functionality
printf("\nLibrary Functionality Test:\n");
unsigned char private_key[32];
unsigned char public_key[32];
if (nostr_generate_keypair(private_key, public_key) == NOSTR_SUCCESS) {
printf("✓ Key generation works\n");
} else {
printf("✗ Key generation failed\n");
}
// Cleanup
nostr_cleanup();
printf("\nVersion test completed successfully.\n");
printf("Note: This library no longer includes runtime version functions\n");
printf(" to avoid build issues when used as a submodule.\n");
return 0;
}
+151
View File
@@ -0,0 +1,151 @@
#!/bin/bash
# increment_and_push.sh - Version increment and git automation script
# Usage: ./increment_and_push.sh "meaningful git comment"
set -e # Exit on error
# Color constants
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
BOLD='\033[1m'
RESET='\033[0m'
# Function to print output with colors
print_info() {
if [ "$USE_COLORS" = true ]; then
echo -e "${BLUE}[INFO]${RESET} $1"
else
echo "[INFO] $1"
fi
}
print_success() {
if [ "$USE_COLORS" = true ]; then
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
else
echo "[SUCCESS] $1"
fi
}
print_warning() {
if [ "$USE_COLORS" = true ]; then
echo -e "${YELLOW}[WARNING]${RESET} $1"
else
echo "[WARNING] $1"
fi
}
print_error() {
if [ "$USE_COLORS" = true ]; then
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
else
echo "[ERROR] $1"
fi
}
# Check if we're in the correct directory
CURRENT_DIR=$(basename "$(pwd)")
if [ "$CURRENT_DIR" != "nostr_core_lib" ]; then
print_error "Script must be run from the nostr_core_lib directory"
echo ""
echo "Current directory: $CURRENT_DIR"
echo "Expected directory: nostr_core_lib"
echo ""
echo "Please change to the nostr_core_lib directory first."
echo ""
exit 1
fi
# Check if git repository exists
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not a git repository. Please initialize git first."
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
COMMIT_MESSAGE="$1"
# Check if nostr_core.h exists
if [ ! -f "nostr_core/nostr_core.h" ]; then
print_error "nostr_core/nostr_core.h not found"
exit 1
fi
print_info "Starting version increment and push process..."
# Extract current version from nostr_core.h
CURRENT_VERSION=$(grep '#define VERSION ' nostr_core/nostr_core.h | cut -d'"' -f2)
if [ -z "$CURRENT_VERSION" ]; then
print_error "Could not find VERSION define in nostr_core.h"
exit 1
fi
# Extract version components
VERSION_MAJOR=$(grep '#define VERSION_MAJOR ' nostr_core/nostr_core.h | awk '{print $3}')
VERSION_MINOR=$(grep '#define VERSION_MINOR ' nostr_core/nostr_core.h | awk '{print $3}')
VERSION_PATCH=$(grep '#define VERSION_PATCH ' nostr_core/nostr_core.h | awk '{print $3}')
if [ -z "$VERSION_MAJOR" ] || [ -z "$VERSION_MINOR" ] || [ -z "$VERSION_PATCH" ]; then
print_error "Could not extract version components from nostr_core.h"
exit 1
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"
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_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
print_success "Updated VERSION file"
fi
# Check git status
if ! git diff --quiet; then
print_info "Adding changes to git..."
git add .
print_info "Committing changes..."
git commit -m "$COMMIT_MESSAGE"
print_success "Changes committed"
else
print_warning "No changes to commit"
fi
# Create and push git tag
print_info "Creating git tag: $NEW_VERSION"
git tag "$NEW_VERSION"
print_info "Pushing commits and tags..."
CURRENT_BRANCH=$(git branch --show-current)
git push origin "$CURRENT_BRANCH"
git push origin "$NEW_VERSION"
print_success "Version $NEW_VERSION successfully released!"
print_info "Git commit: $COMMIT_MESSAGE"
print_info "Tag: $NEW_VERSION"
echo ""
echo "🎉 Release complete! Version $NEW_VERSION is now live."
+395
View File
@@ -0,0 +1,395 @@
/*
* Cashu Mint HTTP Client Implementation
*/
#include "cashu_mint.h"
#include "../cjson/cJSON.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <curl/curl.h>
typedef struct {
char* data;
size_t size;
size_t capacity;
} cashu_http_response_t;
static char* cashu_strdup(const char* s) {
if (!s) return NULL;
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, s, len + 1);
return out;
}
static size_t cashu_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
cashu_http_response_t* response = (cashu_http_response_t*)userp;
size_t total_size = size * nmemb;
if (response->size + total_size + 1 > response->capacity) {
size_t new_capacity = response->capacity ? response->capacity * 2 : 1024;
while (new_capacity < response->size + total_size + 1) {
new_capacity *= 2;
}
char* new_data = (char*)realloc(response->data, new_capacity);
if (!new_data) return 0;
response->data = new_data;
response->capacity = new_capacity;
}
memcpy(response->data + response->size, contents, total_size);
response->size += total_size;
response->data[response->size] = '\0';
return total_size;
}
static int cashu_http_json_request(const char* method,
const char* url,
const char* body,
int timeout_seconds,
char** response_out,
long* status_out) {
if (!method || !url || !response_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*response_out = NULL;
if (status_out) *status_out = 0;
CURL* curl = curl_easy_init();
if (!curl) {
return NOSTR_ERROR_NETWORK_FAILED;
}
cashu_http_response_t response = {0};
response.capacity = 1024;
response.data = (char*)malloc(response.capacity);
if (!response.data) {
curl_easy_cleanup(curl);
return NOSTR_ERROR_MEMORY_FAILED;
}
response.data[0] = '\0';
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Accept: application/json");
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)cashu_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : 10));
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
if (strcmp(method, "POST") == 0) {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "{}");
}
CURLcode res = curl_easy_perform(curl);
long response_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (status_out) *status_out = response_code;
if (res != CURLE_OK) {
free(response.data);
return NOSTR_ERROR_CASHU_HTTP_FAILED;
}
*response_out = response.data;
return NOSTR_SUCCESS;
}
static int cashu_parse_quote_common(cJSON* json,
cashu_mint_quote_t* mint_quote_out,
cashu_melt_quote_t* melt_quote_out) {
if (!json) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
cJSON* quote_id = cJSON_GetObjectItem(json, "quote");
cJSON* request = cJSON_GetObjectItem(json, "request");
cJSON* paid = cJSON_GetObjectItem(json, "paid");
cJSON* amount = cJSON_GetObjectItem(json, "amount");
cJSON* expiry = cJSON_GetObjectItem(json, "expiry");
if (mint_quote_out) {
memset(mint_quote_out, 0, sizeof(*mint_quote_out));
if (quote_id && cJSON_IsString(quote_id)) {
strncpy(mint_quote_out->quote_id, cJSON_GetStringValue(quote_id), sizeof(mint_quote_out->quote_id) - 1);
}
if (request && cJSON_IsString(request)) {
strncpy(mint_quote_out->payment_request, cJSON_GetStringValue(request), sizeof(mint_quote_out->payment_request) - 1);
}
if (paid && cJSON_IsBool(paid)) mint_quote_out->paid = cJSON_IsTrue(paid) ? 1 : 0;
if (amount && cJSON_IsNumber(amount)) mint_quote_out->amount = (uint64_t)cJSON_GetNumberValue(amount);
if (expiry && cJSON_IsNumber(expiry)) mint_quote_out->expiry = (time_t)cJSON_GetNumberValue(expiry);
}
if (melt_quote_out) {
memset(melt_quote_out, 0, sizeof(*melt_quote_out));
if (quote_id && cJSON_IsString(quote_id)) {
strncpy(melt_quote_out->quote_id, cJSON_GetStringValue(quote_id), sizeof(melt_quote_out->quote_id) - 1);
}
if (paid && cJSON_IsBool(paid)) melt_quote_out->paid = cJSON_IsTrue(paid) ? 1 : 0;
if (amount && cJSON_IsNumber(amount)) melt_quote_out->amount = (uint64_t)cJSON_GetNumberValue(amount);
if (expiry && cJSON_IsNumber(expiry)) melt_quote_out->expiry = (time_t)cJSON_GetNumberValue(expiry);
cJSON* fee_reserve = cJSON_GetObjectItem(json, "fee_reserve");
if (fee_reserve && cJSON_IsNumber(fee_reserve)) {
melt_quote_out->fee_reserve = (uint64_t)cJSON_GetNumberValue(fee_reserve);
}
cJSON* preimage = cJSON_GetObjectItem(json, "payment_preimage");
if (preimage && cJSON_IsString(preimage)) {
melt_quote_out->payment_preimage = cashu_strdup(cJSON_GetStringValue(preimage));
}
}
return NOSTR_SUCCESS;
}
static int cashu_call_raw(const char* mint_url,
const char* endpoint,
const char* method,
const char* body,
cJSON** response_out,
int timeout_seconds) {
if (!mint_url || !endpoint || !method || !response_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*response_out = NULL;
char url[1024];
snprintf(url, sizeof(url), "%s%s", mint_url, endpoint);
char* response = NULL;
long status = 0;
int rc = cashu_http_json_request(method, url, body, timeout_seconds, &response, &status);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* json = cJSON_Parse(response ? response : "{}");
free(response);
if (!json) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
if (status < 200 || status >= 300) {
cJSON_Delete(json);
return NOSTR_ERROR_CASHU_MINT_ERROR;
}
*response_out = json;
return NOSTR_SUCCESS;
}
int cashu_mint_get_info(const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds) {
if (!mint_url || !info_out) return NOSTR_ERROR_INVALID_INPUT;
memset(info_out, 0, sizeof(*info_out));
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, "/v1/info", "GET", NULL, &json, timeout_seconds);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* name = cJSON_GetObjectItem(json, "name");
cJSON* pubkey = cJSON_GetObjectItem(json, "pubkey");
cJSON* version = cJSON_GetObjectItem(json, "version");
if (name && cJSON_IsString(name)) info_out->name = cashu_strdup(cJSON_GetStringValue(name));
if (pubkey && cJSON_IsString(pubkey)) info_out->pubkey = cashu_strdup(cJSON_GetStringValue(pubkey));
if (version && cJSON_IsString(version)) info_out->version = cashu_strdup(cJSON_GetStringValue(version));
cJSON* keysets = cJSON_GetObjectItem(json, "keysets");
if (keysets && cJSON_IsArray(keysets)) {
int n = cJSON_GetArraySize(keysets);
if (n > 0) {
info_out->keysets = (cashu_keyset_t*)calloc((size_t)n, sizeof(cashu_keyset_t));
if (!info_out->keysets) {
cJSON_Delete(json);
cashu_mint_free_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < n; i++) {
cJSON* ks = cJSON_GetArrayItem(keysets, i);
if (!ks || !cJSON_IsObject(ks)) continue;
cJSON* id = cJSON_GetObjectItem(ks, "id");
cJSON* unit = cJSON_GetObjectItem(ks, "unit");
cJSON* active = cJSON_GetObjectItem(ks, "active");
if (id && cJSON_IsString(id)) {
strncpy(info_out->keysets[info_out->keyset_count].id,
cJSON_GetStringValue(id), sizeof(info_out->keysets[info_out->keyset_count].id) - 1);
}
if (unit && cJSON_IsString(unit)) {
strncpy(info_out->keysets[info_out->keyset_count].unit,
cJSON_GetStringValue(unit), sizeof(info_out->keysets[info_out->keyset_count].unit) - 1);
}
if (active && cJSON_IsBool(active)) {
info_out->keysets[info_out->keyset_count].active = cJSON_IsTrue(active) ? 1 : 0;
}
info_out->keyset_count++;
}
}
}
cJSON_Delete(json);
return NOSTR_SUCCESS;
}
void cashu_mint_free_info(cashu_mint_info_t* info) {
if (!info) return;
free(info->name);
free(info->pubkey);
free(info->version);
free(info->keysets);
memset(info, 0, sizeof(*info));
}
int cashu_mint_request_mint_quote(const char* mint_url,
uint64_t amount,
const char* unit,
cashu_mint_quote_t* quote_out,
int timeout_seconds) {
if (!mint_url || !unit || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
cJSON* req = cJSON_CreateObject();
cJSON_AddNumberToObject(req, "amount", (double)amount);
cJSON_AddStringToObject(req, "unit", unit);
char* body = cJSON_PrintUnformatted(req);
cJSON_Delete(req);
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, "/v1/mint/quote/bolt11", "POST", body, &json, timeout_seconds);
free(body);
if (rc != NOSTR_SUCCESS) return rc;
rc = cashu_parse_quote_common(json, quote_out, NULL);
cJSON_Delete(json);
return rc;
}
int cashu_mint_check_mint_quote(const char* mint_url,
const char* quote_id,
cashu_mint_quote_t* quote_out,
int timeout_seconds) {
if (!mint_url || !quote_id || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
char endpoint[512];
snprintf(endpoint, sizeof(endpoint), "/v1/mint/quote/bolt11/%s", quote_id);
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, endpoint, "GET", NULL, &json, timeout_seconds);
if (rc != NOSTR_SUCCESS) return rc;
rc = cashu_parse_quote_common(json, quote_out, NULL);
cJSON_Delete(json);
return rc;
}
int cashu_mint_request_melt_quote(const char* mint_url,
const char* payment_request,
const char* unit,
cashu_melt_quote_t* quote_out,
int timeout_seconds) {
if (!mint_url || !payment_request || !unit || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
cJSON* req = cJSON_CreateObject();
cJSON_AddStringToObject(req, "request", payment_request);
cJSON_AddStringToObject(req, "unit", unit);
char* body = cJSON_PrintUnformatted(req);
cJSON_Delete(req);
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, "/v1/melt/quote/bolt11", "POST", body, &json, timeout_seconds);
free(body);
if (rc != NOSTR_SUCCESS) return rc;
rc = cashu_parse_quote_common(json, NULL, quote_out);
cJSON_Delete(json);
return rc;
}
int cashu_mint_check_melt_quote(const char* mint_url,
const char* quote_id,
cashu_melt_quote_t* quote_out,
int timeout_seconds) {
if (!mint_url || !quote_id || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
char endpoint[512];
snprintf(endpoint, sizeof(endpoint), "/v1/melt/quote/bolt11/%s", quote_id);
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, endpoint, "GET", NULL, &json, timeout_seconds);
if (rc != NOSTR_SUCCESS) return rc;
rc = cashu_parse_quote_common(json, NULL, quote_out);
cJSON_Delete(json);
return rc;
}
int cashu_mint_swap(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds) {
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
char* body = cJSON_PrintUnformatted(request_body);
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
int rc = cashu_call_raw(mint_url, "/v1/swap", "POST", body, response_out, timeout_seconds);
free(body);
return rc;
}
int cashu_mint_mint_tokens(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds) {
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
char* body = cJSON_PrintUnformatted(request_body);
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
int rc = cashu_call_raw(mint_url, "/v1/mint/bolt11", "POST", body, response_out, timeout_seconds);
free(body);
return rc;
}
int cashu_mint_melt_tokens(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds) {
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
char* body = cJSON_PrintUnformatted(request_body);
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
int rc = cashu_call_raw(mint_url, "/v1/melt/bolt11", "POST", body, response_out, timeout_seconds);
free(body);
return rc;
}
int cashu_mint_check_proofs_state(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds) {
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
char* body = cJSON_PrintUnformatted(request_body);
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
int rc = cashu_call_raw(mint_url, "/v1/checkstate", "POST", body, response_out, timeout_seconds);
free(body);
return rc;
}
+102
View File
@@ -0,0 +1,102 @@
/*
* Cashu Mint HTTP Client
*/
#ifndef NOSTR_CASHU_MINT_H
#define NOSTR_CASHU_MINT_H
#include <stdint.h>
#include <time.h>
#include "nostr_common.h"
#include "nip060.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CASHU_API_VERSION "v1"
typedef struct {
char id[17];
char unit[16];
int active;
} cashu_keyset_t;
typedef struct {
char* name;
char* pubkey;
char* version;
cashu_keyset_t* keysets;
int keyset_count;
} cashu_mint_info_t;
typedef struct {
char quote_id[128];
char payment_request[2048];
int paid;
uint64_t amount;
time_t expiry;
} cashu_mint_quote_t;
typedef struct {
char quote_id[128];
uint64_t amount;
uint64_t fee_reserve;
int paid;
char* payment_preimage;
time_t expiry;
} cashu_melt_quote_t;
int cashu_mint_get_info(const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds);
void cashu_mint_free_info(cashu_mint_info_t* info);
int cashu_mint_request_mint_quote(const char* mint_url,
uint64_t amount,
const char* unit,
cashu_mint_quote_t* quote_out,
int timeout_seconds);
int cashu_mint_check_mint_quote(const char* mint_url,
const char* quote_id,
cashu_mint_quote_t* quote_out,
int timeout_seconds);
int cashu_mint_request_melt_quote(const char* mint_url,
const char* payment_request,
const char* unit,
cashu_melt_quote_t* quote_out,
int timeout_seconds);
int cashu_mint_check_melt_quote(const char* mint_url,
const char* quote_id,
cashu_melt_quote_t* quote_out,
int timeout_seconds);
int cashu_mint_swap(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds);
int cashu_mint_mint_tokens(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds);
int cashu_mint_melt_tokens(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds);
int cashu_mint_check_proofs_state(const char* mint_url,
cJSON* request_body,
cJSON** response_out,
int timeout_seconds);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_CASHU_MINT_H */
-824
View File
@@ -1,824 +0,0 @@
/*
* NOSTR Core Library Implementation - Core Functionality
*
* Self-contained crypto implementation (no external crypto dependencies)
*
* This file contains:
* - NIP-19: Bech32-encoded Entities
* - NIP-01: Basic Protocol Flow
* - NIP-06: Key Derivation from Mnemonic
* - NIP-10: Text Notes (Kind 1)
* - NIP-13: Proof of Work
* - General Utilities
* - Identity Management
* - Single Relay Communication
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>
// Our self-contained crypto implementation
#include "nostr_crypto.h"
// Our production-ready WebSocket implementation
#include "../nostr_websocket/nostr_websocket_tls.h"
// cJSON for JSON handling
#include "../cjson/cJSON.h"
// Forward declarations for bech32 functions (used by NIP-06 functions)
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad);
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len);
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// GENERAL UTILITIES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex) {
for (size_t i = 0; i < len; i++) {
sprintf(hex + i * 2, "%02x", bytes[i]);
}
hex[len * 2] = '\0';
}
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len) {
if (strlen(hex) != len * 2) {
return NOSTR_ERROR_INVALID_INPUT;
}
for (size_t i = 0; i < len; i++) {
if (sscanf(hex + i * 2, "%02hhx", &bytes[i]) != 1) {
return NOSTR_ERROR_INVALID_INPUT;
}
}
return NOSTR_SUCCESS;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-01: BASIC PROTOCOL FLOW
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
int nostr_init(void) {
if (nostr_crypto_init() != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
void nostr_cleanup(void) {
nostr_crypto_cleanup();
}
const char* nostr_strerror(int error_code) {
switch (error_code) {
case NOSTR_SUCCESS: return "Success";
case NOSTR_ERROR_INVALID_INPUT: return "Invalid input";
case NOSTR_ERROR_CRYPTO_FAILED: return "Cryptographic operation failed";
case NOSTR_ERROR_MEMORY_FAILED: return "Memory allocation failed";
case NOSTR_ERROR_IO_FAILED: return "I/O operation failed";
case NOSTR_ERROR_NETWORK_FAILED: return "Network operation failed";
case NOSTR_ERROR_NIP04_INVALID_FORMAT: return "NIP-04 invalid format";
case NOSTR_ERROR_NIP04_DECRYPT_FAILED: return "NIP-04 decryption failed";
case NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL: return "NIP-04 buffer too small";
default: return "Unknown error";
}
}
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
if (!private_key) {
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) {
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();
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);
return NULL;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
cJSON_Delete(event);
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);
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;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-06: KEY DERIVATION FROM MNEMONIC
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
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) {
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;
}
// Generate public key from private key (already x-only for NOSTR)
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key) {
if (!mnemonic || mnemonic_size < 256 || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate entropy for 12-word mnemonic
unsigned char entropy[16];
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
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;
}
// Derive keys from the generated mnemonic
return nostr_derive_keys_from_mnemonic(mnemonic, account, private_key, public_key);
}
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key) {
if (!mnemonic || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate mnemonic
if (nostr_bip39_mnemonic_validate(mnemonic) != 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Convert mnemonic to seed
unsigned char seed[64];
if (nostr_bip39_mnemonic_to_seed(mnemonic, "", seed, sizeof(seed)) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Derive master key from seed
nostr_hd_key_t master_key;
if (nostr_bip32_key_from_seed(seed, sizeof(seed), &master_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// NIP-06 path: m/44'/1237'/account'/0/0
nostr_hd_key_t derived_key;
uint32_t path[] = {
0x80000000 + 44, // 44' (hardened)
0x80000000 + 1237, // 1237' (hardened)
0x80000000 + account, // account' (hardened)
0, // 0 (not hardened)
0 // 0 (not hardened)
};
if (nostr_bip32_derive_path(&master_key, path, sizeof(path) / sizeof(path[0]), &derived_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Extract private key and public key
memcpy(private_key, derived_key.private_key, 32);
memcpy(public_key, derived_key.public_key + 1, 32); // Remove compression prefix for x-only
return NOSTR_SUCCESS;
}
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output) {
if (!key || !hrp || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
uint8_t conv[64];
size_t conv_len;
if (!convert_bits(conv, &conv_len, 5, key, 32, 8, 1)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
if (!bech32_encode(output, hrp, conv, conv_len)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
nostr_input_type_t nostr_detect_input_type(const char* input) {
if (!input || strlen(input) == 0) {
return NOSTR_INPUT_UNKNOWN;
}
size_t len = strlen(input);
// Check for bech32 nsec
if (len > 5 && strncmp(input, "nsec1", 5) == 0) {
return NOSTR_INPUT_NSEC_BECH32;
}
// Check for hex nsec (64 characters, all hex)
if (len == 64) {
int is_hex = 1;
for (size_t i = 0; i < len; i++) {
if (!isxdigit(input[i])) {
is_hex = 0;
break;
}
}
if (is_hex) {
return NOSTR_INPUT_NSEC_HEX;
}
}
// Check for mnemonic (space-separated words)
int word_count = 0;
char temp[1024];
strncpy(temp, input, sizeof(temp) - 1);
temp[sizeof(temp) - 1] = '\0';
char* token = strtok(temp, " ");
while (token != NULL) {
word_count++;
token = strtok(NULL, " ");
}
// BIP39 mnemonics are typically 12, 18, or 24 words
if (word_count >= 12 && word_count <= 24) {
return NOSTR_INPUT_MNEMONIC;
}
return NOSTR_INPUT_UNKNOWN;
}
int nostr_decode_nsec(const char* input, unsigned char* private_key) {
if (!input || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
nostr_input_type_t type = nostr_detect_input_type(input);
if (type == NOSTR_INPUT_NSEC_HEX) {
if (nostr_hex_to_bytes(input, private_key, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else if (type == NOSTR_INPUT_NSEC_BECH32) {
size_t decoded_len;
if (!bech32_decode(input, "nsec", private_key, &decoded_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (decoded_len != 32) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate the private key
if (nostr_ec_private_key_verify(private_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-13: PROOF OF WORK
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Count leading zero bits in a hash (NIP-13 reference implementation)
*/
static int zero_bits(unsigned char b) {
int n = 0;
if (b == 0)
return 8;
while (b >>= 1)
n++;
return 7-n;
}
/**
* Find the number of leading zero bits in a hash (NIP-13 reference implementation)
*/
static int count_leading_zero_bits(unsigned char *hash) {
int bits, total, i;
for (i = 0, total = 0; i < 32; i++) {
bits = zero_bits(hash[i]);
total += bits;
if (bits != 8)
break;
}
return total;
}
/**
* Add or update nonce tag with target difficulty
*/
static int update_nonce_tag_with_difficulty(cJSON* tags, uint64_t nonce, int target_difficulty) {
if (!tags) return -1;
// Look for existing nonce tag and remove it
cJSON* tag = NULL;
int index = 0;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (tag_type && cJSON_IsString(tag_type) &&
strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) {
// Remove existing nonce tag
cJSON_DetachItemFromArray(tags, index);
cJSON_Delete(tag);
break;
}
}
index++;
}
// Add new nonce tag with format: ["nonce", "<nonce>", "<target_difficulty>"]
cJSON* nonce_tag = cJSON_CreateArray();
if (!nonce_tag) return -1;
char nonce_str[32];
char difficulty_str[16];
snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce);
snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty);
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str));
cJSON_AddItemToArray(tags, nonce_tag);
return 0;
}
/**
* Helper function to replace event content with successful PoW result
*/
static void replace_event_content(cJSON* target_event, cJSON* source_event) {
// Remove old fields
cJSON_DeleteItemFromObject(target_event, "id");
cJSON_DeleteItemFromObject(target_event, "sig");
cJSON_DeleteItemFromObject(target_event, "tags");
cJSON_DeleteItemFromObject(target_event, "created_at");
// Copy new fields from successful event
cJSON* id = cJSON_GetObjectItem(source_event, "id");
cJSON* sig = cJSON_GetObjectItem(source_event, "sig");
cJSON* tags = cJSON_GetObjectItem(source_event, "tags");
cJSON* created_at = cJSON_GetObjectItem(source_event, "created_at");
if (id) cJSON_AddItemToObject(target_event, "id", cJSON_Duplicate(id, 1));
if (sig) cJSON_AddItemToObject(target_event, "sig", cJSON_Duplicate(sig, 1));
if (tags) cJSON_AddItemToObject(target_event, "tags", cJSON_Duplicate(tags, 1));
if (created_at) cJSON_AddItemToObject(target_event, "created_at", cJSON_Duplicate(created_at, 1));
}
/**
* Add NIP-13 Proof of Work to an event
*
* @param event The event to add proof of work to
* @param private_key The private key for re-signing the event
* @param target_difficulty Target number of leading zero bits (default: 4 if 0)
* @param progress_callback Optional callback for mining progress
* @param user_data User data for progress callback
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty,
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
void* user_data) {
if (!event || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Set default difficulty if not specified (but allow 0 to disable PoW)
if (target_difficulty < 0) {
target_difficulty = 4;
}
// If target_difficulty is 0, skip proof of work entirely
if (target_difficulty == 0) {
return NOSTR_SUCCESS;
}
// Extract event data for reconstruction
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
if (!kind_item || !content_item || !created_at_item || !tags_item) {
return NOSTR_ERROR_INVALID_INPUT;
}
int kind = (int)cJSON_GetNumberValue(kind_item);
const char* content = cJSON_GetStringValue(content_item);
time_t original_timestamp = (time_t)cJSON_GetNumberValue(created_at_item);
uint64_t nonce = 0;
int attempts = 0;
int max_attempts = 10000000;
time_t current_timestamp = original_timestamp;
// PoW difficulty tracking variables
int best_difficulty_this_round = 0;
int best_difficulty_overall = 0;
// Mining loop
while (attempts < max_attempts) {
// Update timestamp every 10,000 iterations
if (attempts % 10000 == 0) {
current_timestamp = time(NULL);
#ifdef ENABLE_DEBUG_LOGGING
FILE* f = fopen("debug.log", "a");
if (f) {
fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n",
attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty);
fclose(f);
}
#endif
// Reset best difficulty for the new round
best_difficulty_this_round = 0;
}
// Create working copy of tags and add nonce
cJSON* working_tags = cJSON_Duplicate(tags_item, 1);
if (!working_tags) {
return NOSTR_ERROR_MEMORY_FAILED;
}
if (update_nonce_tag_with_difficulty(working_tags, nonce, target_difficulty) != 0) {
cJSON_Delete(working_tags);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Create and sign new event with current nonce
cJSON* test_event = nostr_create_and_sign_event(kind, content, working_tags,
private_key, current_timestamp);
cJSON_Delete(working_tags);
if (!test_event) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Check PoW difficulty
cJSON* id_item = cJSON_GetObjectItem(test_event, "id");
if (!id_item || !cJSON_IsString(id_item)) {
cJSON_Delete(test_event);
return NOSTR_ERROR_CRYPTO_FAILED;
}
const char* event_id = cJSON_GetStringValue(id_item);
unsigned char hash[32];
if (nostr_hex_to_bytes(event_id, hash, 32) != NOSTR_SUCCESS) {
cJSON_Delete(test_event);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Count leading zero bits using NIP-13 method
int current_difficulty = count_leading_zero_bits(hash);
// Update difficulty tracking
if (current_difficulty > best_difficulty_this_round) {
best_difficulty_this_round = current_difficulty;
}
if (current_difficulty > best_difficulty_overall) {
best_difficulty_overall = current_difficulty;
}
// Call progress callback if provided
if (progress_callback) {
progress_callback(current_difficulty, nonce, user_data);
}
// Check if we've reached the target
if (current_difficulty >= target_difficulty) {
#ifdef ENABLE_DEBUG_LOGGING
FILE* f = fopen("debug.log", "a");
if (f) {
fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n",
current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1);
// Print the final event JSON
char* event_json = cJSON_Print(test_event);
if (event_json) {
fprintf(f, "Final event: %s\n", event_json);
free(event_json);
}
fclose(f);
}
#endif
// Copy successful result back to input event
replace_event_content(event, test_event);
cJSON_Delete(test_event);
return NOSTR_SUCCESS;
}
cJSON_Delete(test_event);
nonce++;
attempts++;
}
#ifdef ENABLE_DEBUG_LOGGING
// Debug logging - failure
FILE* f = fopen("debug.log", "a");
if (f) {
fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts);
fclose(f);
}
#endif
// If we reach here, we've exceeded max attempts
return NOSTR_ERROR_CRYPTO_FAILED;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-19: BECH32-ENCODED ENTITIES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
#define BECH32_CONST 1
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
static const int8_t bech32_charset_rev[128] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
static uint32_t bech32_polymod_step(uint32_t pre) {
uint8_t b = pre >> 25;
return ((pre & 0x1FFFFFF) << 5) ^
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
(-((b >> 1) & 1) & 0x26508e6dUL) ^
(-((b >> 2) & 1) & 0x1ea119faUL) ^
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
(-((b >> 4) & 1) & 0x2a1462b3UL);
}
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
*outlen = 0;
while (inlen--) {
val = (val << inbits) | *(in++);
bits += inbits;
while (bits >= outbits) {
bits -= outbits;
out[(*outlen)++] = (val >> bits) & maxv;
}
}
if (pad) {
if (bits) {
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
}
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
return 0;
}
return 1;
}
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
uint32_t chk = 1;
size_t i, hrp_len = strlen(hrp);
for (i = 0; i < hrp_len; ++i) {
int ch = hrp[i];
if (ch < 33 || ch > 126) return 0;
if (ch >= 'A' && ch <= 'Z') return 0;
chk = bech32_polymod_step(chk) ^ (ch >> 5);
}
chk = bech32_polymod_step(chk);
for (i = 0; i < hrp_len; ++i) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
*(output++) = hrp[i];
}
*(output++) = '1';
for (i = 0; i < data_len; ++i) {
if (*data >> 5) return 0;
chk = bech32_polymod_step(chk) ^ (*data);
*(output++) = bech32_charset[*(data++)];
}
for (i = 0; i < 6; ++i) {
chk = bech32_polymod_step(chk);
}
chk ^= BECH32_CONST;
for (i = 0; i < 6; ++i) {
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
}
*output = 0;
return 1;
}
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
if (!input || !hrp || !data || !data_len) {
return 0;
}
size_t input_len = strlen(input);
size_t hrp_len = strlen(hrp);
if (input_len < hrp_len + 7) return 0;
if (strncmp(input, hrp, hrp_len) != 0) return 0;
if (input[hrp_len] != '1') return 0;
const char* data_part = input + hrp_len + 1;
size_t data_part_len = input_len - hrp_len - 1;
uint8_t values[256];
for (size_t i = 0; i < data_part_len; i++) {
unsigned char c = (unsigned char)data_part[i];
if (c >= 128) return 0;
int8_t val = bech32_charset_rev[c];
if (val == -1) return 0;
values[i] = (uint8_t)val;
}
if (data_part_len < 6) return 0;
uint32_t chk = 1;
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
}
chk = bech32_polymod_step(chk);
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
}
for (size_t i = 0; i < data_part_len; i++) {
chk = bech32_polymod_step(chk) ^ values[i];
}
if (chk != BECH32_CONST) return 0;
size_t payload_len = data_part_len - 6;
size_t decoded_len;
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
return 0;
}
*data_len = decoded_len;
return 1;
}
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1067 -329
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+116 -29
View File
@@ -26,6 +26,9 @@
// cJSON for JSON handling
#include "../cjson/cJSON.h"
// NIP-42 Authentication
#include "nip042.h"
// =============================================================================
// TYPE DEFINITIONS FOR SYNCHRONOUS RELAY QUERIES
// =============================================================================
@@ -51,6 +54,12 @@ typedef struct {
cJSON** events; // Array of events from this relay
int events_capacity; // Allocated capacity
char subscription_id[32]; // Unique subscription ID
// NIP-42 Authentication fields
nostr_auth_state_t auth_state; // Current authentication state
char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; // Stored challenge
time_t auth_challenge_time; // When challenge was received
int nip42_enabled; // Whether NIP-42 is enabled for this relay
} relay_connection_t;
@@ -65,7 +74,9 @@ cJSON** synchronous_query_relays_with_progress(
int* result_count,
int relay_timeout_seconds,
relay_progress_callback_t callback,
void* user_data) {
void* user_data,
int nip42_enabled,
const unsigned char* private_key) {
if (!relay_urls || relay_count <= 0 || !filter || !result_count) {
if (result_count) *result_count = 0;
@@ -95,11 +106,17 @@ cJSON** synchronous_query_relays_with_progress(
relays[i].last_activity = start_time;
relays[i].events_capacity = 10;
relays[i].events = malloc(relays[i].events_capacity * sizeof(cJSON*));
// Initialize NIP-42 authentication fields
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
relays[i].auth_challenge_time = 0;
relays[i].nip42_enabled = nip42_enabled;
// Generate unique subscription ID
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
"sync_%d_%ld", i, start_time);
if (callback) {
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
}
@@ -191,19 +208,50 @@ cJSON** synchronous_query_relays_with_progress(
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
if (relay->nip42_enabled && private_key && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(challenge_json)) {
const char* challenge = cJSON_GetStringValue(challenge_json);
// Store challenge and attempt authentication
strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1);
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
relay->auth_challenge_time = time(NULL);
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
// Create and send authentication event
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, private_key, 0);
if (auth_event) {
char* auth_message = nostr_nip42_create_auth_message(auth_event);
if (auth_message) {
if (nostr_ws_send_text(relay->client, auth_message) >= 0) {
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
if (callback) {
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
}
}
free(auth_message);
}
cJSON_Delete(auth_event);
}
}
}
} else if (msg_type && strcmp(msg_type, "EVENT") == 0) {
// Handle EVENT message
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
cJSON* event = cJSON_GetArrayItem(parsed, 2);
if (cJSON_IsString(sub_id_json) && event &&
strcmp(cJSON_GetStringValue(sub_id_json), relay->subscription_id) == 0) {
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
if (event_id_json && cJSON_IsString(event_id_json)) {
const char* event_id = cJSON_GetStringValue(event_id_json);
// Check for duplicate
int is_duplicate = 0;
for (int j = 0; j < seen_count; j++) {
@@ -212,31 +260,31 @@ cJSON** synchronous_query_relays_with_progress(
break;
}
}
if (!is_duplicate && seen_count < 1000) {
// New event - add to seen list
strncpy(seen_event_ids[seen_count], event_id, 64);
seen_event_ids[seen_count][64] = '\0';
seen_count++;
total_unique_events++;
// Store event in relay's array
if (relay->events_received >= relay->events_capacity) {
relay->events_capacity *= 2;
relay->events = realloc(relay->events,
relay->events = realloc(relay->events,
relay->events_capacity * sizeof(cJSON*));
}
relay->events[relay->events_received] = cJSON_Duplicate(event, 1);
relay->events_received++;
relay->state = RELAY_STATE_ACTIVE;
if (callback) {
callback(relay->url, "event_found", event_id,
relay->events_received, relay_count,
callback(relay->url, "event_found", event_id,
relay->events_received, relay_count,
completed_relays, user_data);
}
// For FIRST_RESULT mode, return immediately
if (mode == RELAY_QUERY_FIRST_RESULT) {
result_array = malloc(sizeof(cJSON*));
@@ -244,7 +292,7 @@ cJSON** synchronous_query_relays_with_progress(
result_array[0] = cJSON_Duplicate(event, 1);
*result_count = 1;
if (callback) {
callback(NULL, "first_result", event_id,
callback(NULL, "first_result", event_id,
1, relay_count, completed_relays, user_data);
}
}
@@ -254,7 +302,7 @@ cJSON** synchronous_query_relays_with_progress(
}
}
}
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
// Handle End of Stored Events
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
@@ -400,7 +448,9 @@ publish_result_t* synchronous_publish_event_with_progress(
int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback,
void* user_data) {
void* user_data,
int nip42_enabled,
const unsigned char* private_key) {
if (!relay_urls || relay_count <= 0 || !event || !success_count) {
if (success_count) *success_count = 0;
@@ -443,7 +493,13 @@ publish_result_t* synchronous_publish_event_with_progress(
relays[i].state = RELAY_STATE_CONNECTING;
relays[i].last_activity = start_time;
results[i] = PUBLISH_ERROR; // Default to error
// Initialize NIP-42 authentication fields
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
relays[i].auth_challenge_time = 0;
relays[i].nip42_enabled = nip42_enabled;
if (callback) {
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
}
@@ -535,34 +591,65 @@ publish_result_t* synchronous_publish_event_with_progress(
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "OK") == 0) {
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
if (relay->nip42_enabled && private_key && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(challenge_json)) {
const char* challenge = cJSON_GetStringValue(challenge_json);
// Store challenge and attempt authentication
strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1);
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
relay->auth_challenge_time = time(NULL);
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
// Create and send authentication event
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, private_key, 0);
if (auth_event) {
char* auth_message = nostr_nip42_create_auth_message(auth_event);
if (auth_message) {
if (nostr_ws_send_text(relay->client, auth_message) >= 0) {
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
if (callback) {
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
}
}
free(auth_message);
}
cJSON_Delete(auth_event);
}
}
}
} else if (msg_type && strcmp(msg_type, "OK") == 0) {
// Handle OK message: ["OK", <event_id>, <true|false>, <message>]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* ok_event_id = cJSON_GetArrayItem(parsed, 1);
cJSON* accepted = cJSON_GetArrayItem(parsed, 2);
cJSON* message = cJSON_GetArrayItem(parsed, 3);
// Verify this OK is for our event
if (ok_event_id && cJSON_IsString(ok_event_id) && event_id &&
strcmp(cJSON_GetStringValue(ok_event_id), event_id) == 0) {
relay->state = RELAY_STATE_EOSE_RECEIVED; // Reuse for "completed"
active_relays--;
completed_relays++;
const char* ok_message = "";
if (message && cJSON_IsString(message)) {
ok_message = cJSON_GetStringValue(message);
}
if (accepted && cJSON_IsBool(accepted) && cJSON_IsTrue(accepted)) {
// Event accepted
results[i] = PUBLISH_SUCCESS;
(*success_count)++;
if (callback) {
callback(relay->url, "accepted", ok_message,
callback(relay->url, "accepted", ok_message,
*success_count, relay_count, completed_relays, user_data);
}
} else {
@@ -570,11 +657,11 @@ publish_result_t* synchronous_publish_event_with_progress(
results[i] = PUBLISH_REJECTED;
if (callback) {
callback(relay->url, "rejected", ok_message,
callback(relay->url, "rejected", ok_message,
*success_count, relay_count, completed_relays, user_data);
}
}
// Close connection
nostr_ws_close(relay->client);
relay->client = NULL;
Binary file not shown.
@@ -1,15 +1,76 @@
/*
* NOSTR AES Implementation
*
*
* Based on tiny-AES-c by kokke (public domain)
* Configured specifically for NIP-04: AES-256-CBC only
*
*
* This is an implementation of the AES algorithm, specifically CBC mode.
* Configured for AES-256 as required by NIP-04.
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h> // CBC mode, for memset
#include "nostr_aes.h"
// Configure for NIP-04 requirements: AES-256-CBC only
#define CBC 1
#define ECB 0
#define CTR 0
// Configure for AES-256 (required by NIP-04)
#define AES128 0
#define AES192 0
#define AES256 1
#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only
#if defined(AES256) && (AES256 == 1)
#define AES_KEYLEN 32
#define AES_keyExpSize 240
#elif defined(AES192) && (AES192 == 1)
#define AES_KEYLEN 24
#define AES_keyExpSize 208
#else
#define AES_KEYLEN 16 // Key length in bytes
#define AES_keyExpSize 176
#endif
struct AES_ctx
{
uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
// state - array holding the intermediate results during decryption.
typedef uint8_t state_t[4][4];
// Function prototypes (internal use only)
static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key);
static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey);
static void SubBytes(state_t* state);
static void ShiftRows(state_t* state);
static uint8_t xtime(uint8_t x);
static void MixColumns(state_t* state);
static void InvMixColumns(state_t* state);
static void InvSubBytes(state_t* state);
static void InvShiftRows(state_t* state);
static void Cipher(state_t* state, const uint8_t* RoundKey);
static void InvCipher(state_t* state, const uint8_t* RoundKey);
static void XorWithIv(uint8_t* buf, const uint8_t* Iv);
// Public functions (used by NIP-04 implementation)
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif
#if defined(CBC) && (CBC == 1)
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif
// The number of columns comprising a state in AES. This is a constant in AES. Value=4
#define Nb 4
@@ -1,15 +1,47 @@
/*
* nostr_chacha20.c - ChaCha20 stream cipher implementation
*
*
* Implementation based on RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols"
*
*
* This implementation is adapted from the RFC 8439 reference specification.
* It prioritizes correctness and clarity over performance optimization.
*/
#include "nostr_chacha20.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
/*
* ============================================================================
* CONSTANTS AND DEFINITIONS
* ============================================================================
*/
#define CHACHA20_KEY_SIZE 32 /* 256 bits */
#define CHACHA20_NONCE_SIZE 12 /* 96 bits */
#define CHACHA20_BLOCK_SIZE 64 /* 512 bits */
/*
* ============================================================================
* FUNCTION PROTOTYPES (INTERNAL USE ONLY)
* ============================================================================
*/
// Internal utility functions
static uint32_t bytes_to_u32_le(const uint8_t *bytes);
static void u32_to_bytes_le(uint32_t val, uint8_t *bytes);
// Public functions (used by NIP-44 implementation)
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d);
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);
void chacha20_init_state(uint32_t state[16], const uint8_t key[32],
uint32_t counter, const uint8_t nonce[12]);
void chacha20_serialize_state(const uint32_t state[16], uint8_t output[64]);
/*
* ============================================================================
* UTILITY MACROS AND FUNCTIONS
@@ -1,4 +1,3 @@
#include "nostr_secp256k1.h"
#include <secp256k1.h>
#include <secp256k1_schnorrsig.h>
#include <secp256k1_ecdh.h>
@@ -6,6 +5,33 @@
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
/*
* PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED
* These functions are for internal library use only.
*/
/** Opaque data structure that holds a parsed and valid public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_pubkey {
unsigned char data[64];
} nostr_secp256k1_pubkey;
/** Opaque data structure that holds a parsed keypair.
* Guaranteed to be 96 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_keypair {
unsigned char data[96];
} nostr_secp256k1_keypair;
/** Opaque data structure that holds a parsed x-only public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_xonly_pubkey {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
// Global context for secp256k1 operations
static secp256k1_context* g_ctx = NULL;
+345
View File
@@ -0,0 +1,345 @@
/*
* NOSTR Core Library - NIP-001: Basic Protocol Flow
*
* Event creation, signing, serialization and core protocol functions
*/
#include "nip001.h"
#include "utils.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nostr_common.h"
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
typedef struct {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey* pubkey, const unsigned char* input32);
int nostr_secp256k1_schnorrsig_verify(const unsigned char* sig64, const unsigned char* msg32, const nostr_secp256k1_xonly_pubkey* pubkey);
// Declare utility functions
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex);
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len);
int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash);
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature);
/**
* 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) {
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) {
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();
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);
return NULL;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
cJSON_Delete(event);
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);
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;
}
/**
* Validate the structure of a NOSTR event
* Checks required fields, types, and basic format validation
*/
int nostr_validate_event_structure(cJSON* event) {
if (!event || !cJSON_IsObject(event)) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Check required fields exist
cJSON* id_item = cJSON_GetObjectItem(event, "id");
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");
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
!tags_item || !content_item || !sig_item) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Validate field types
if (!cJSON_IsString(id_item)) return NOSTR_ERROR_EVENT_INVALID_ID;
if (!cJSON_IsString(pubkey_item)) return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
if (!cJSON_IsNumber(created_at_item)) return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
if (!cJSON_IsNumber(kind_item)) return NOSTR_ERROR_EVENT_INVALID_KIND;
if (!cJSON_IsArray(tags_item)) return NOSTR_ERROR_EVENT_INVALID_TAGS;
if (!cJSON_IsString(content_item)) return NOSTR_ERROR_EVENT_INVALID_CONTENT;
if (!cJSON_IsString(sig_item)) return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
// Validate hex string lengths
const char* id_str = cJSON_GetStringValue(id_item);
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
const char* sig_str = cJSON_GetStringValue(sig_item);
if (!id_str || strlen(id_str) != 64) return NOSTR_ERROR_EVENT_INVALID_ID;
if (!pubkey_str || strlen(pubkey_str) != 64) return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
if (!sig_str || strlen(sig_str) != 128) return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
// Validate hex characters (lowercase)
for (int i = 0; i < 64; i++) {
char c = id_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_ID;
}
c = pubkey_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
}
// Validate signature hex characters (lowercase) - 128 characters
for (int i = 0; i < 128; i++) {
char c = sig_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
}
}
// Validate created_at is a valid timestamp (positive number)
double created_at = cJSON_GetNumberValue(created_at_item);
if (created_at < 0) return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
// Validate kind is valid (0-65535)
double kind = cJSON_GetNumberValue(kind_item);
if (kind < 0 || kind > 65535 || kind != (int)kind) {
return NOSTR_ERROR_EVENT_INVALID_KIND;
}
// Validate tags array structure (array of arrays of strings)
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
cJSON* tag_element;
cJSON_ArrayForEach(tag_element, tag_item) {
if (!cJSON_IsString(tag_element)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
}
}
return NOSTR_SUCCESS;
}
/**
* Verify the cryptographic signature of a NOSTR event
* Validates event ID and signature according to NIP-01
*/
int nostr_verify_event_signature(cJSON* event) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Get event fields
cJSON* id_item = cJSON_GetObjectItem(event, "id");
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");
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
!tags_item || !content_item || !sig_item) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* 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));
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
cJSON_Delete(serialize_array);
if (!serialize_string) {
return NOSTR_ERROR_MEMORY_FAILED;
}
// 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);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Convert hash to hex for event ID verification
char calculated_id[65];
nostr_bytes_to_hex(event_hash, 32, calculated_id);
// Compare with provided event ID
const char* provided_id = cJSON_GetStringValue(id_item);
if (!provided_id || strcmp(calculated_id, provided_id) != 0) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_ID;
}
// Verify signature
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
const char* sig_str = cJSON_GetStringValue(sig_item);
if (!pubkey_str || !sig_str) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Convert hex strings to bytes
unsigned char pubkey_bytes[32];
unsigned char sig_bytes[64];
if (nostr_hex_to_bytes(pubkey_str, pubkey_bytes, 32) != 0 ||
nostr_hex_to_bytes(sig_str, sig_bytes, 64) != 0) {
free(serialize_string);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Parse the public key into secp256k1 format
nostr_secp256k1_xonly_pubkey xonly_pubkey;
if (!nostr_secp256k1_xonly_pubkey_parse(&xonly_pubkey, pubkey_bytes)) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
// Verify Schnorr signature
if (!nostr_secp256k1_schnorrsig_verify(sig_bytes, event_hash, &xonly_pubkey)) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
}
free(serialize_string);
return NOSTR_SUCCESS;
}
/**
* Complete validation of a NOSTR event
* Performs both structure and cryptographic validation
*/
int nostr_validate_event(cJSON* event) {
// First validate structure (fast check)
int structure_result = nostr_validate_event_structure(event);
if (structure_result != NOSTR_SUCCESS) {
return structure_result;
}
// Then verify signature (expensive check)
return nostr_verify_event_signature(event);
}
+23
View File
@@ -0,0 +1,23 @@
/*
* NOSTR Core Library - NIP-001: Basic Protocol Flow
*
* Event creation, signing, serialization and core protocol functions
*/
#ifndef NIP001_H
#define NIP001_H
#include <stdint.h>
#include <time.h>
#include "../cjson/cJSON.h"
// Function declarations
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp);
// Event validation functions
int nostr_validate_event_structure(cJSON* event);
int nostr_verify_event_signature(cJSON* event);
int nostr_validate_event(cJSON* event);
#endif // NIP001_H
+347
View File
@@ -0,0 +1,347 @@
/*
* NIP-04: Encrypted Direct Message Implementation
* https://github.com/nostr-protocol/nips/blob/master/04.md
*/
#include "nip004.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
int ecdh_shared_secret(const unsigned char* private_key, const unsigned char* public_key, unsigned char* shared_secret);
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// AES context and functions for NIP-04 encryption
struct AES_ctx {
unsigned char RoundKey[240]; // AES-256 key expansion
unsigned char Iv[16]; // Initialization vector
};
void AES_init_ctx_iv(struct AES_ctx* ctx, const unsigned char* key, const unsigned char* iv);
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, unsigned char* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, unsigned char* buf, size_t length);
// Forward declarations for internal functions
static int aes_cbc_encrypt(const unsigned char* key, const unsigned char* iv,
const unsigned char* input, size_t input_len,
unsigned char* output);
static int aes_cbc_decrypt(const unsigned char* key, const unsigned char* iv,
const unsigned char* input, size_t input_len,
unsigned char* output);
static size_t pkcs7_pad(unsigned char* data, size_t data_len, size_t block_size);
static size_t pkcs7_unpad(unsigned char* data, size_t data_len);
// Memory clearing utility
static void memory_clear(const void *p, size_t len) {
if (p && len) {
memset((void *)p, 0, len);
}
}
// =============================================================================
// AES-256-CBC ENCRYPTION/DECRYPTION USING TINYAES
// =============================================================================
static int aes_cbc_encrypt(const unsigned char* key, const unsigned char* iv,
const unsigned char* input, size_t input_len,
unsigned char* output) {
if (!key || !iv || !input || !output || input_len % 16 != 0) {
return -1;
}
// Initialize AES context with key and IV
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
// Copy input to output (tinyAES works in-place)
memcpy(output, input, input_len);
// Encrypt using AES-256-CBC
AES_CBC_encrypt_buffer(&ctx, output, input_len);
return 0;
}
static int aes_cbc_decrypt(const unsigned char* key, const unsigned char* iv,
const unsigned char* input, size_t input_len,
unsigned char* output) {
if (!key || !iv || !input || !output || input_len % 16 != 0) {
return -1;
}
// Initialize AES context with key and IV
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
// Copy input to output (tinyAES works in-place)
memcpy(output, input, input_len);
// Decrypt using AES-256-CBC
AES_CBC_decrypt_buffer(&ctx, output, input_len);
return 0;
}
// PKCS#7 padding functions
static size_t pkcs7_pad(unsigned char* data, size_t data_len, size_t block_size) {
size_t padding = block_size - (data_len % block_size);
for (size_t i = 0; i < padding; i++) {
data[data_len + i] = (unsigned char)padding;
}
return data_len + padding;
}
static size_t pkcs7_unpad(unsigned char* data, size_t data_len) {
if (data_len == 0) return 0;
unsigned char padding = data[data_len - 1];
if (padding == 0 || padding > 16) return 0; // Invalid padding
// Verify padding
for (size_t i = data_len - padding; i < data_len; i++) {
if (data[i] != padding) return 0; // Invalid padding
}
return data_len - padding;
}
// =============================================================================
// NIP-04 IMPLEMENTATION
// =============================================================================
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size) {
if (!sender_private_key || !recipient_public_key || !plaintext || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
size_t plaintext_len = strlen(plaintext);
if (plaintext_len > NOSTR_NIP04_MAX_PLAINTEXT_SIZE) {
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
}
// FIX: Calculate final size requirements EARLY before any allocations
// CRITICAL: Account for PKCS#7 padding which ALWAYS adds 1-16 bytes
// If plaintext_len is a multiple of 16, PKCS#7 adds a full 16-byte block
size_t padded_len = ((plaintext_len / 16) + 1) * 16; // Always add one full block for PKCS#7
size_t ciphertext_b64_max = ((padded_len + 2) / 3) * 4 + 1;
size_t iv_b64_max = ((16 + 2) / 3) * 4 + 1; // Always 25 bytes
size_t estimated_result_len = ciphertext_b64_max + 4 + iv_b64_max; // +4 for "?iv="
// FIX: Check output buffer size BEFORE doing any work
if (estimated_result_len > output_size) {
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
}
// Step 1: Compute ECDH shared secret
unsigned char shared_secret[32];
if (ecdh_shared_secret(sender_private_key, recipient_public_key, shared_secret) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 2: Generate random IV (16 bytes)
unsigned char iv[16];
if (nostr_secp256k1_get_random_bytes(iv, 16) != 1) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 3: Pad plaintext using PKCS#7
unsigned char* padded_data = malloc(padded_len);
if (!padded_data) {
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(padded_data, plaintext, plaintext_len);
size_t actual_padded_len = pkcs7_pad(padded_data, plaintext_len, 16);
// Step 4: Encrypt using AES-256-CBC
unsigned char* ciphertext = malloc(padded_len);
if (!ciphertext) {
free(padded_data);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (aes_cbc_encrypt(shared_secret, iv, padded_data, actual_padded_len, ciphertext) != 0) {
free(padded_data);
free(ciphertext);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 5: Base64 encode ciphertext and IV
size_t ciphertext_b64_len = ((actual_padded_len + 2) / 3) * 4 + 1;
size_t iv_b64_len = ((16 + 2) / 3) * 4 + 1;
char* ciphertext_b64 = malloc(ciphertext_b64_len);
char* iv_b64 = malloc(iv_b64_len);
if (!ciphertext_b64 || !iv_b64) {
free(padded_data);
free(ciphertext);
free(ciphertext_b64);
free(iv_b64);
return NOSTR_ERROR_MEMORY_FAILED;
}
// FIX: Pass buffer sizes to base64_encode and check for success
size_t ct_b64_len = base64_encode(ciphertext, actual_padded_len, ciphertext_b64, ciphertext_b64_len);
size_t iv_b64_len_actual = base64_encode(iv, 16, iv_b64, iv_b64_len);
// FIX: Check if encoding succeeded
if (ct_b64_len == 0 || iv_b64_len_actual == 0) {
free(padded_data);
free(ciphertext);
free(ciphertext_b64);
free(iv_b64);
memory_clear(shared_secret, 32);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 6: Format as "ciphertext?iv=iv_base64" - size check moved earlier, now guaranteed to fit
size_t result_len = ct_b64_len + 4 + iv_b64_len_actual + 1; // +4 for "?iv=", +1 for null
if (result_len > output_size) {
free(padded_data);
free(ciphertext);
free(ciphertext_b64);
free(iv_b64);
memory_clear(shared_secret, 32);
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
}
snprintf(output, output_size, "%s?iv=%s", ciphertext_b64, iv_b64);
// Cleanup
memory_clear(shared_secret, 32);
memory_clear(padded_data, padded_len);
memory_clear(ciphertext, padded_len);
free(padded_data);
free(ciphertext);
free(ciphertext_b64);
free(iv_b64);
return NOSTR_SUCCESS;
}
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size) {
if (!recipient_private_key || !sender_public_key || !encrypted_data || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Step 1: Parse encrypted data format "ciphertext?iv=iv_base64"
char* separator = strstr(encrypted_data, "?iv=");
if (!separator) {
return NOSTR_ERROR_NIP04_INVALID_FORMAT;
}
size_t ciphertext_b64_len = separator - encrypted_data;
const char* iv_b64 = separator + 4; // Skip "?iv="
if (ciphertext_b64_len == 0 || strlen(iv_b64) == 0) {
return NOSTR_ERROR_NIP04_INVALID_FORMAT;
}
// Step 2: Create null-terminated copy of ciphertext base64
char* ciphertext_b64 = malloc(ciphertext_b64_len + 1);
if (!ciphertext_b64) {
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(ciphertext_b64, encrypted_data, ciphertext_b64_len);
ciphertext_b64[ciphertext_b64_len] = '\0';
// Step 3: Calculate proper buffer sizes for decoded data
// Base64 decoding: 4 chars -> 3 bytes, so max decoded size is (len * 3) / 4
size_t max_ciphertext_len = ((ciphertext_b64_len + 3) / 4) * 3;
size_t max_iv_len = ((strlen(iv_b64) + 3) / 4) * 3;
// Allocate buffers with proper sizes
unsigned char* ciphertext = malloc(max_ciphertext_len);
unsigned char* iv_buffer = malloc(max_iv_len);
if (!ciphertext || !iv_buffer) {
free(ciphertext_b64);
free(ciphertext);
free(iv_buffer);
return NOSTR_ERROR_MEMORY_FAILED;
}
// Step 4: Base64 decode ciphertext and IV
size_t ciphertext_len = base64_decode(ciphertext_b64, ciphertext);
size_t iv_len = base64_decode(iv_b64, iv_buffer);
if (ciphertext_len == 0 || iv_len != 16 || ciphertext_len % 16 != 0) {
free(ciphertext_b64);
free(ciphertext);
free(iv_buffer);
return NOSTR_ERROR_NIP04_INVALID_FORMAT;
}
// Copy IV to fixed-size buffer for safety
unsigned char iv[16];
memcpy(iv, iv_buffer, 16);
free(iv_buffer);
// Step 5: Compute ECDH shared secret
unsigned char shared_secret[32];
if (ecdh_shared_secret(recipient_private_key, sender_public_key, shared_secret) != 0) {
free(ciphertext_b64);
free(ciphertext);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 6: Decrypt using AES-256-CBC
unsigned char* plaintext_padded = malloc(ciphertext_len);
if (!plaintext_padded) {
free(ciphertext_b64);
free(ciphertext);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (aes_cbc_decrypt(shared_secret, iv, ciphertext, ciphertext_len, plaintext_padded) != 0) {
free(ciphertext_b64);
free(ciphertext);
free(plaintext_padded);
return NOSTR_ERROR_NIP04_DECRYPT_FAILED;
}
// Step 7: Remove PKCS#7 padding
size_t plaintext_len = pkcs7_unpad(plaintext_padded, ciphertext_len);
if (plaintext_len == 0 || plaintext_len > ciphertext_len) {
free(ciphertext_b64);
free(ciphertext);
free(plaintext_padded);
return NOSTR_ERROR_NIP04_DECRYPT_FAILED;
}
// Step 8: Copy to output buffer and null-terminate
if (plaintext_len + 1 > output_size) {
free(ciphertext_b64);
free(ciphertext);
free(plaintext_padded);
return NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL;
}
memcpy(output, plaintext_padded, plaintext_len);
output[plaintext_len] = '\0';
// Cleanup
memory_clear(shared_secret, 32);
memory_clear(plaintext_padded, ciphertext_len);
free(ciphertext_b64);
free(ciphertext);
free(plaintext_padded);
return NOSTR_SUCCESS;
}
+56
View File
@@ -0,0 +1,56 @@
/*
* NIP-04: Encrypted Direct Message
* https://github.com/nostr-protocol/nips/blob/master/04.md
*/
#ifndef NOSTR_NIP004_H
#define NOSTR_NIP004_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// NIP-04 constants
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB
// NIP-04 Constants
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB
// #define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
/**
* NIP-04: Encrypt a message using ECDH + AES-256-CBC
*
* @param sender_private_key 32-byte sender private key
* @param recipient_public_key 32-byte recipient public key (x-only)
* @param plaintext Message to encrypt
* @param output Buffer for encrypted output (format: "ciphertext?iv=iv_base64")
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
/**
* NIP-04: Decrypt a message using ECDH + AES-256-CBC
*
* @param recipient_private_key 32-byte recipient private key
* @param sender_public_key 32-byte sender public key (x-only)
* @param encrypted_data Encrypted message (format: "ciphertext?iv=iv_base64")
* @param output Buffer for decrypted plaintext
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP004_H
+344
View File
@@ -0,0 +1,344 @@
/*
* NOSTR Core Library - NIP-005: Mapping Nostr keys to DNS-based internet identifiers
*/
#include "nip005.h"
#include "../cjson/cJSON.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> // For strcasecmp
#include <ctype.h>
#include <curl/curl.h>
// Structure for HTTP response handling
typedef struct {
char* data;
size_t size;
size_t capacity;
} nip05_http_response_t;
/**
* Callback function for curl to write HTTP response data
*/
static size_t nip05_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
nip05_http_response_t* response = (nip05_http_response_t*)userp;
size_t total_size = size * nmemb;
// Check if we need to expand the buffer
if (response->size + total_size >= response->capacity) {
size_t new_capacity = response->capacity * 2;
if (new_capacity < response->size + total_size + 1) {
new_capacity = response->size + total_size + 1;
}
char* new_data = realloc(response->data, new_capacity);
if (!new_data) {
return 0; // Out of memory
}
response->data = new_data;
response->capacity = new_capacity;
}
// Append the new data
memcpy(response->data + response->size, contents, total_size);
response->size += total_size;
response->data[response->size] = '\0';
return total_size;
}
/**
* Parse and validate a NIP-05 identifier into local part and domain
*/
static int nip05_parse_identifier(const char* identifier, char* local_part, char* domain) {
if (!identifier || !local_part || !domain) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Find the @ symbol
const char* at_pos = strchr(identifier, '@');
if (!at_pos) {
return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER;
}
// Extract local part
size_t local_len = at_pos - identifier;
if (local_len == 0 || local_len >= 64) {
return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER;
}
strncpy(local_part, identifier, local_len);
local_part[local_len] = '\0';
// Extract domain
const char* domain_start = at_pos + 1;
size_t domain_len = strlen(domain_start);
if (domain_len == 0 || domain_len >= 256) {
return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER;
}
strcpy(domain, domain_start);
// Validate characters in local part (a-z0-9-_.)
for (size_t i = 0; i < local_len; i++) {
char c = local_part[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.')) {
return NOSTR_ERROR_NIP05_INVALID_IDENTIFIER;
}
}
return NOSTR_SUCCESS;
}
/**
* Make HTTP GET request to a URL
*/
static int nip05_http_get(const char* url, int timeout_seconds, char** response_data) {
if (!url || !response_data) {
return NOSTR_ERROR_INVALID_INPUT;
}
CURL* curl = curl_easy_init();
if (!curl) {
return NOSTR_ERROR_NETWORK_FAILED;
}
nip05_http_response_t response = {0};
response.capacity = 1024;
response.data = malloc(response.capacity);
if (!response.data) {
curl_easy_cleanup(curl);
return NOSTR_ERROR_MEMORY_FAILED;
}
response.data[0] = '\0';
// Set curl options with proper type casting to fix warnings
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip05_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT));
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // NIP-05 forbids redirects
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
// Perform the request
CURLcode res = curl_easy_perform(curl);
long response_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
free(response.data);
return NOSTR_ERROR_NIP05_HTTP_FAILED;
}
if (response_code != 200) {
free(response.data);
return NOSTR_ERROR_NIP05_HTTP_FAILED;
}
*response_data = response.data;
return NOSTR_SUCCESS;
}
/**
* Validate that a hex string is a valid public key
*/
static int nip05_validate_pubkey_hex(const char* hex_pubkey) {
if (!hex_pubkey) {
return NOSTR_ERROR_INVALID_INPUT;
}
size_t len = strlen(hex_pubkey);
if (len != 64) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check all characters are valid hex
for (size_t i = 0; i < len; i++) {
char c = hex_pubkey[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
return NOSTR_ERROR_INVALID_INPUT;
}
}
return NOSTR_SUCCESS;
}
/**
* Parse a .well-known/nostr.json response and extract pubkey and relays for a specific name
*/
int nostr_nip05_parse_well_known(const char* json_response, const char* local_part,
char* pubkey_hex_out, char*** relays, int* relay_count) {
if (!json_response || !local_part) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize outputs
if (pubkey_hex_out) {
pubkey_hex_out[0] = '\0';
}
if (relays) {
*relays = NULL;
}
if (relay_count) {
*relay_count = 0;
}
// Parse JSON
cJSON* json = cJSON_Parse(json_response);
if (!json) {
return NOSTR_ERROR_NIP05_JSON_PARSE_FAILED;
}
int result = NOSTR_ERROR_NIP05_NAME_NOT_FOUND;
// Get the "names" object
cJSON* names = cJSON_GetObjectItem(json, "names");
if (names && cJSON_IsObject(names)) {
cJSON* pubkey_item = cJSON_GetObjectItem(names, local_part);
if (pubkey_item && cJSON_IsString(pubkey_item)) {
const char* found_pubkey = cJSON_GetStringValue(pubkey_item);
// Validate the public key format
if (nip05_validate_pubkey_hex(found_pubkey) == NOSTR_SUCCESS) {
if (pubkey_hex_out) {
strcpy(pubkey_hex_out, found_pubkey);
}
result = NOSTR_SUCCESS;
// Extract relays if requested
if (relays && relay_count) {
cJSON* relays_obj = cJSON_GetObjectItem(json, "relays");
if (relays_obj && cJSON_IsObject(relays_obj)) {
cJSON* user_relays = cJSON_GetObjectItem(relays_obj, found_pubkey);
if (user_relays && cJSON_IsArray(user_relays)) {
int relay_array_size = cJSON_GetArraySize(user_relays);
if (relay_array_size > 0) {
char** relay_array = malloc(relay_array_size * sizeof(char*));
if (relay_array) {
int valid_relays = 0;
for (int i = 0; i < relay_array_size; i++) {
cJSON* relay_item = cJSON_GetArrayItem(user_relays, i);
if (relay_item && cJSON_IsString(relay_item)) {
const char* relay_url = cJSON_GetStringValue(relay_item);
if (relay_url && strlen(relay_url) > 0) {
relay_array[valid_relays] = malloc(strlen(relay_url) + 1);
if (relay_array[valid_relays]) {
strcpy(relay_array[valid_relays], relay_url);
valid_relays++;
}
}
}
}
if (valid_relays > 0) {
*relays = relay_array;
*relay_count = valid_relays;
} else {
free(relay_array);
}
}
}
}
}
}
}
}
}
cJSON_Delete(json);
return result;
}
/**
* Lookup a public key from a NIP-05 identifier
*/
int nostr_nip05_lookup(const char* nip05_identifier, char* pubkey_hex_out,
char*** relays, int* relay_count, int timeout_seconds) {
if (!nip05_identifier) {
return NOSTR_ERROR_INVALID_INPUT;
}
char local_part[64];
char domain[256];
char url[NOSTR_MAX_URL_SIZE];
// Parse the identifier
int parse_result = nip05_parse_identifier(nip05_identifier, local_part, domain);
if (parse_result != NOSTR_SUCCESS) {
return parse_result;
}
// Construct the .well-known URL
int url_result = snprintf(url, sizeof(url), "https://%s/.well-known/nostr.json?name=%s",
domain, local_part);
if (url_result >= (int)sizeof(url) || url_result < 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Make HTTP request
char* response_data = NULL;
int http_result = nip05_http_get(url, timeout_seconds, &response_data);
if (http_result != NOSTR_SUCCESS) {
return http_result;
}
// Parse the response
int parse_response_result = nostr_nip05_parse_well_known(response_data, local_part,
pubkey_hex_out, relays, relay_count);
free(response_data);
return parse_response_result;
}
/**
* Verify a NIP-05 identifier against a public key
*/
int nostr_nip05_verify(const char* nip05_identifier, const char* pubkey_hex,
char*** relays, int* relay_count, int timeout_seconds) {
if (!nip05_identifier || !pubkey_hex) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate the input public key format
if (nip05_validate_pubkey_hex(pubkey_hex) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
char found_pubkey[65];
// Lookup the public key for this identifier
int lookup_result = nostr_nip05_lookup(nip05_identifier, found_pubkey,
relays, relay_count, timeout_seconds);
if (lookup_result != NOSTR_SUCCESS) {
return lookup_result;
}
// Compare the public keys (case insensitive)
if (strcasecmp(pubkey_hex, found_pubkey) != 0) {
// Clean up relays if verification failed
if (relays && *relays) {
for (int i = 0; i < (relay_count ? *relay_count : 0); i++) {
free((*relays)[i]);
}
free(*relays);
*relays = NULL;
}
if (relay_count) {
*relay_count = 0;
}
return NOSTR_ERROR_NIP05_PUBKEY_MISMATCH;
}
return NOSTR_SUCCESS;
}
+18
View File
@@ -0,0 +1,18 @@
/*
* NOSTR Core Library - NIP-005: Mapping Nostr keys to DNS-based internet identifiers
*/
#ifndef NIP005_H
#define NIP005_H
#include "nip001.h"
// Function declarations
int nostr_nip05_parse_well_known(const char* json_response, const char* local_part,
char* pubkey_hex_out, char*** relays, int* relay_count);
int nostr_nip05_lookup(const char* nip05_identifier, char* pubkey_hex_out,
char*** relays, int* relay_count, int timeout_seconds);
int nostr_nip05_verify(const char* nip05_identifier, const char* pubkey_hex,
char*** relays, int* relay_count, int timeout_seconds);
#endif // NIP005_H
+118
View File
@@ -0,0 +1,118 @@
/*
* NOSTR Core Library - NIP-006: Key Derivation from Mnemonic
*/
#include "nip006.h"
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include "../nostr_core/nostr_common.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) {
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;
}
// Generate public key from private key (already x-only for NOSTR)
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key) {
if (!mnemonic || mnemonic_size < 256 || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate entropy for 12-word mnemonic
unsigned char entropy[16];
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
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;
}
// Derive keys from the generated mnemonic
return nostr_derive_keys_from_mnemonic(mnemonic, account, private_key, public_key);
}
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key) {
if (!mnemonic || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate mnemonic
if (nostr_bip39_mnemonic_validate(mnemonic) != 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Convert mnemonic to seed
unsigned char seed[64];
if (nostr_bip39_mnemonic_to_seed(mnemonic, "", seed, sizeof(seed)) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Derive master key from seed
nostr_hd_key_t master_key;
if (nostr_bip32_key_from_seed(seed, sizeof(seed), &master_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// NIP-06 path: m/44'/1237'/account'/0/0
nostr_hd_key_t derived_key;
uint32_t path[] = {
0x80000000 + 44, // 44' (hardened)
0x80000000 + 1237, // 1237' (hardened)
0x80000000 + account, // account' (hardened)
0, // 0 (not hardened)
0 // 0 (not hardened)
};
if (nostr_bip32_derive_path(&master_key, path, sizeof(path) / sizeof(path[0]), &derived_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Extract private key and public key
memcpy(private_key, derived_key.private_key, 32);
memcpy(public_key, derived_key.public_key + 1, 32); // Remove compression prefix for x-only
return NOSTR_SUCCESS;
}
// Note: nostr_detect_input_type, nostr_decode_nsec, and nostr_decode_npub
// are implemented in NIP-019 to avoid multiple definitions
+30
View File
@@ -0,0 +1,30 @@
/*
* NOSTR Core Library - NIP-006: Key Derivation from Mnemonic
*/
#ifndef NIP006_H
#define NIP006_H
#include "nip001.h"
#include <stdint.h>
// Input type detection
typedef enum {
NOSTR_INPUT_UNKNOWN = 0,
NOSTR_INPUT_NSEC_HEX,
NOSTR_INPUT_NSEC_BECH32,
NOSTR_INPUT_MNEMONIC
} nostr_input_type_t;
// Function declarations
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key);
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key);
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key);
nostr_input_type_t nostr_detect_input_type(const char* input);
int nostr_decode_nsec(const char* input, unsigned char* private_key);
int nostr_decode_npub(const char* input, unsigned char* public_key);
#endif // NIP006_H
+473
View File
@@ -0,0 +1,473 @@
/*
* NOSTR Core Library - NIP-011: Relay Information Document
*/
#include "nip011.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core/nostr_common.h"
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
#include <curl/curl.h>
// Maximum sizes for NIP-11 operations
#define NIP11_MAX_URL_SIZE 512
#define NIP11_MAX_RESPONSE_SIZE 16384
#define NIP11_DEFAULT_TIMEOUT 10
// Structure for HTTP response handling (same as NIP-05)
typedef struct {
char* data;
size_t size;
size_t capacity;
} nip11_http_response_t;
/**
* Callback function for curl to write HTTP response data
*/
static size_t nip11_write_callback(void* contents, size_t size, size_t nmemb, nip11_http_response_t* response) {
size_t total_size = size * nmemb;
// Check if we need to expand the buffer
if (response->size + total_size >= response->capacity) {
size_t new_capacity = response->capacity * 2;
if (new_capacity < response->size + total_size + 1) {
new_capacity = response->size + total_size + 1;
}
char* new_data = realloc(response->data, new_capacity);
if (!new_data) {
return 0; // Out of memory
}
response->data = new_data;
response->capacity = new_capacity;
}
// Append the new data
memcpy(response->data + response->size, contents, total_size);
response->size += total_size;
response->data[response->size] = '\0';
return total_size;
}
/**
* Convert WebSocket URL to HTTP URL for NIP-11 document retrieval
*/
static char* nip11_ws_to_http_url(const char* ws_url) {
if (!ws_url) {
return NULL;
}
size_t url_len = strlen(ws_url);
char* http_url = malloc(url_len + 10); // Extra space for protocol change
if (!http_url) {
return NULL;
}
// Convert ws:// to http:// and wss:// to https://
if (strncmp(ws_url, "ws://", 5) == 0) {
sprintf(http_url, "http://%s", ws_url + 5);
} else if (strncmp(ws_url, "wss://", 6) == 0) {
sprintf(http_url, "https://%s", ws_url + 6);
} else {
// Assume it's already HTTP(S) or add https:// as default
if (strncmp(ws_url, "http://", 7) == 0 || strncmp(ws_url, "https://", 8) == 0) {
strcpy(http_url, ws_url);
} else {
sprintf(http_url, "https://%s", ws_url);
}
}
return http_url;
}
/**
* Parse supported NIPs array from JSON
*/
static int nip11_parse_supported_nips(cJSON* nips_array, int** nips_out, size_t* count_out) {
if (!nips_array || !cJSON_IsArray(nips_array)) {
*nips_out = NULL;
*count_out = 0;
return NOSTR_SUCCESS;
}
int array_size = cJSON_GetArraySize(nips_array);
if (array_size == 0) {
*nips_out = NULL;
*count_out = 0;
return NOSTR_SUCCESS;
}
int* nips = malloc(array_size * sizeof(int));
if (!nips) {
return NOSTR_ERROR_MEMORY_FAILED;
}
int valid_count = 0;
for (int i = 0; i < array_size; i++) {
cJSON* nip_item = cJSON_GetArrayItem(nips_array, i);
if (nip_item && cJSON_IsNumber(nip_item)) {
nips[valid_count++] = (int)cJSON_GetNumberValue(nip_item);
}
}
if (valid_count == 0) {
free(nips);
*nips_out = NULL;
*count_out = 0;
} else {
*nips_out = nips;
*count_out = valid_count;
}
return NOSTR_SUCCESS;
}
/**
* Parse string array from JSON
*/
static int nip11_parse_string_array(cJSON* json_array, char*** strings_out, size_t* count_out) {
if (!json_array || !cJSON_IsArray(json_array)) {
*strings_out = NULL;
*count_out = 0;
return NOSTR_SUCCESS;
}
int array_size = cJSON_GetArraySize(json_array);
if (array_size == 0) {
*strings_out = NULL;
*count_out = 0;
return NOSTR_SUCCESS;
}
char** strings = malloc(array_size * sizeof(char*));
if (!strings) {
return NOSTR_ERROR_MEMORY_FAILED;
}
int valid_count = 0;
for (int i = 0; i < array_size; i++) {
cJSON* string_item = cJSON_GetArrayItem(json_array, i);
if (string_item && cJSON_IsString(string_item)) {
const char* str_value = cJSON_GetStringValue(string_item);
if (str_value && strlen(str_value) > 0) {
strings[valid_count] = malloc(strlen(str_value) + 1);
if (strings[valid_count]) {
strcpy(strings[valid_count], str_value);
valid_count++;
}
}
}
}
if (valid_count == 0) {
free(strings);
*strings_out = NULL;
*count_out = 0;
} else {
*strings_out = strings;
*count_out = valid_count;
}
return NOSTR_SUCCESS;
}
/**
* Helper to safely copy JSON string value
*/
static char* nip11_copy_json_string(cJSON* json_item) {
if (!json_item || !cJSON_IsString(json_item)) {
return NULL;
}
const char* str_value = cJSON_GetStringValue(json_item);
if (!str_value) {
return NULL;
}
char* copy = malloc(strlen(str_value) + 1);
if (copy) {
strcpy(copy, str_value);
}
return copy;
}
/**
* Parse NIP-11 relay information document from JSON
*/
static int nip11_parse_relay_info(const char* json_response, nostr_relay_info_t* info) {
if (!json_response || !info) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize structure
memset(info, 0, sizeof(nostr_relay_info_t));
// Parse JSON
cJSON* json = cJSON_Parse(json_response);
if (!json) {
return NOSTR_ERROR_NIP05_JSON_PARSE_FAILED;
}
// Parse basic information
info->basic.name = nip11_copy_json_string(cJSON_GetObjectItem(json, "name"));
info->basic.description = nip11_copy_json_string(cJSON_GetObjectItem(json, "description"));
info->basic.pubkey = nip11_copy_json_string(cJSON_GetObjectItem(json, "pubkey"));
info->basic.contact = nip11_copy_json_string(cJSON_GetObjectItem(json, "contact"));
info->basic.software = nip11_copy_json_string(cJSON_GetObjectItem(json, "software"));
info->basic.version = nip11_copy_json_string(cJSON_GetObjectItem(json, "version"));
// Parse supported NIPs
cJSON* supported_nips = cJSON_GetObjectItem(json, "supported_nips");
nip11_parse_supported_nips(supported_nips, &info->basic.supported_nips, &info->basic.supported_nips_count);
// Parse limitations (if present)
cJSON* limitation = cJSON_GetObjectItem(json, "limitation");
if (limitation && cJSON_IsObject(limitation)) {
info->has_limitations = 1;
cJSON* item;
item = cJSON_GetObjectItem(limitation, "max_message_length");
info->limitations.max_message_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "max_subscriptions");
info->limitations.max_subscriptions = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "max_filters");
info->limitations.max_filters = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "max_limit");
info->limitations.max_limit = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "max_subid_length");
info->limitations.max_subid_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "min_prefix");
info->limitations.min_prefix = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "max_event_tags");
info->limitations.max_event_tags = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "max_content_length");
info->limitations.max_content_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "min_pow_difficulty");
info->limitations.min_pow_difficulty = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "auth_required");
info->limitations.auth_required = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
item = cJSON_GetObjectItem(limitation, "payment_required");
info->limitations.payment_required = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
item = cJSON_GetObjectItem(limitation, "restricted_writes");
info->limitations.restricted_writes = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
item = cJSON_GetObjectItem(limitation, "created_at_lower_limit");
info->limitations.created_at_lower_limit = (item && cJSON_IsNumber(item)) ? (long)cJSON_GetNumberValue(item) : -1;
item = cJSON_GetObjectItem(limitation, "created_at_upper_limit");
info->limitations.created_at_upper_limit = (item && cJSON_IsNumber(item)) ? (long)cJSON_GetNumberValue(item) : -1;
}
// Parse relay countries
cJSON* relay_countries = cJSON_GetObjectItem(json, "relay_countries");
if (relay_countries && cJSON_IsArray(relay_countries)) {
info->has_content_limitations = 1;
nip11_parse_string_array(relay_countries, &info->content_limitations.relay_countries,
&info->content_limitations.relay_countries_count);
}
// Parse community preferences
cJSON* language_tags = cJSON_GetObjectItem(json, "language_tags");
cJSON* tags = cJSON_GetObjectItem(json, "tags");
cJSON* posting_policy = cJSON_GetObjectItem(json, "posting_policy");
if ((language_tags && cJSON_IsArray(language_tags)) ||
(tags && cJSON_IsArray(tags)) ||
(posting_policy && cJSON_IsString(posting_policy))) {
info->has_community_preferences = 1;
if (language_tags) {
nip11_parse_string_array(language_tags, &info->community_preferences.language_tags,
&info->community_preferences.language_tags_count);
}
if (tags) {
nip11_parse_string_array(tags, &info->community_preferences.tags,
&info->community_preferences.tags_count);
}
if (posting_policy) {
info->community_preferences.posting_policy = nip11_copy_json_string(posting_policy);
}
}
// Parse icon
cJSON* icon = cJSON_GetObjectItem(json, "icon");
if (icon && cJSON_IsString(icon)) {
info->has_icon = 1;
info->icon.icon = nip11_copy_json_string(icon);
}
cJSON_Delete(json);
return NOSTR_SUCCESS;
}
/**
* Fetch relay information document from a relay URL
*/
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds) {
if (!relay_url || !info_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Convert WebSocket URL to HTTP URL
char* http_url = nip11_ws_to_http_url(relay_url);
if (!http_url) {
return NOSTR_ERROR_MEMORY_FAILED;
}
// Allocate info structure
nostr_relay_info_t* info = malloc(sizeof(nostr_relay_info_t));
if (!info) {
free(http_url);
return NOSTR_ERROR_MEMORY_FAILED;
}
// Make HTTP request with NIP-11 required header
CURL* curl = curl_easy_init();
if (!curl) {
free(http_url);
free(info);
return NOSTR_ERROR_NETWORK_FAILED;
}
// Use the HTTP response structure
nip11_http_response_t response = {0};
response.capacity = 1024;
response.data = malloc(response.capacity);
if (!response.data) {
curl_easy_cleanup(curl);
free(http_url);
free(info);
return NOSTR_ERROR_MEMORY_FAILED;
}
response.data[0] = '\0';
// Set up headers for NIP-11
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Accept: application/nostr+json");
// Set curl options - use proper function pointer cast
curl_easy_setopt(curl, CURLOPT_URL, http_url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip11_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT));
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // NIP-11 allows redirects
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
// Perform the request
CURLcode res = curl_easy_perform(curl);
long response_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
free(http_url);
if (res != CURLE_OK || response_code != 200) {
free(response.data);
free(info);
return NOSTR_ERROR_NIP05_HTTP_FAILED;
}
// Parse the relay information
int parse_result = nip11_parse_relay_info(response.data, info);
free(response.data);
if (parse_result != NOSTR_SUCCESS) {
nostr_nip11_relay_info_free(info);
return parse_result;
}
*info_out = info;
return NOSTR_SUCCESS;
}
/**
* Free relay information structure
*/
void nostr_nip11_relay_info_free(nostr_relay_info_t* info) {
if (!info) {
return;
}
// Free basic info strings
free(info->basic.name);
free(info->basic.description);
free(info->basic.pubkey);
free(info->basic.contact);
free(info->basic.software);
free(info->basic.version);
free(info->basic.supported_nips);
// Free content limitations
if (info->has_content_limitations) {
for (size_t i = 0; i < info->content_limitations.relay_countries_count; i++) {
free(info->content_limitations.relay_countries[i]);
}
free(info->content_limitations.relay_countries);
}
// Free community preferences
if (info->has_community_preferences) {
for (size_t i = 0; i < info->community_preferences.language_tags_count; i++) {
free(info->community_preferences.language_tags[i]);
}
free(info->community_preferences.language_tags);
for (size_t i = 0; i < info->community_preferences.tags_count; i++) {
free(info->community_preferences.tags[i]);
}
free(info->community_preferences.tags);
free(info->community_preferences.posting_policy);
}
// Free icon
if (info->has_icon) {
free(info->icon.icon);
}
free(info);
}
#else // DISABLE_NIP05
/**
* Stub implementations when NIP-05 is disabled at compile time
*/
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds) {
(void)relay_url;
(void)info_out;
(void)timeout_seconds;
return NOSTR_ERROR_NETWORK_FAILED; // NIP-11 disabled at compile time
}
void nostr_nip11_relay_info_free(nostr_relay_info_t* info) {
(void)info;
// No-op when disabled
}
#endif // DISABLE_NIP05
+84
View File
@@ -0,0 +1,84 @@
/*
* NOSTR Core Library - NIP-011: Relay Information Document
*/
#ifndef NIP011_H
#define NIP011_H
#include "nip001.h"
#include <stddef.h>
// NIP-11 Relay Information structures
// Basic relay information
typedef struct {
char* name;
char* description;
char* pubkey;
char* contact;
char* software;
char* version;
int* supported_nips;
size_t supported_nips_count;
} nostr_relay_basic_info_t;
// Relay limitations
typedef struct {
int max_message_length;
int max_subscriptions;
int max_filters;
int max_limit;
int max_subid_length;
int min_prefix;
int max_event_tags;
int max_content_length;
int min_pow_difficulty;
int auth_required; // -1 = not specified, 0 = false, 1 = true
int payment_required; // -1 = not specified, 0 = false, 1 = true
int restricted_writes; // -1 = not specified, 0 = false, 1 = true
long created_at_lower_limit;
long created_at_upper_limit;
} nostr_relay_limitations_t;
// Content limitations
typedef struct {
char** relay_countries;
size_t relay_countries_count;
} nostr_relay_content_limitations_t;
// Community preferences
typedef struct {
char** language_tags;
size_t language_tags_count;
char** tags;
size_t tags_count;
char* posting_policy;
} nostr_relay_community_preferences_t;
// Icon information
typedef struct {
char* icon;
} nostr_relay_icon_t;
// Complete relay information structure
typedef struct {
nostr_relay_basic_info_t basic;
int has_limitations;
nostr_relay_limitations_t limitations;
int has_content_limitations;
nostr_relay_content_limitations_t content_limitations;
int has_community_preferences;
nostr_relay_community_preferences_t community_preferences;
int has_icon;
nostr_relay_icon_t icon;
} nostr_relay_info_t;
// Function declarations
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds);
void nostr_nip11_relay_info_free(nostr_relay_info_t* info);
#endif // NIP011_H
+613
View File
@@ -0,0 +1,613 @@
/*
* NOSTR Core Library - NIP-013: Proof of Work
*/
#include "nip013.h"
#include "nip001.h"
#include "utils.h"
#include "../cjson/cJSON.h"
#include "nostr_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include "../nostr_core/nostr_common.h"
/**
* Count leading zero bits in a hash (NIP-13 reference implementation)
*/
static int zero_bits(unsigned char b) {
int n = 0;
if (b == 0)
return 8;
while (b >>= 1)
n++;
return 7-n;
}
/**
* Find the number of leading zero bits in a hash (NIP-13 reference implementation)
*/
static int count_leading_zero_bits(unsigned char *hash) {
int bits, total, i;
for (i = 0, total = 0; i < 32; i++) {
bits = zero_bits(hash[i]);
total += bits;
if (bits != 8)
break;
}
return total;
}
/**
* Add or update nonce tag with target difficulty
*/
static int update_nonce_tag_with_difficulty(cJSON* tags, uint64_t nonce, int target_difficulty) {
if (!tags) return -1;
// Look for existing nonce tag and remove it
cJSON* tag = NULL;
int index = 0;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (tag_type && cJSON_IsString(tag_type) &&
strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) {
// Remove existing nonce tag
cJSON_DetachItemFromArray(tags, index);
cJSON_Delete(tag);
break;
}
}
index++;
}
// Add new nonce tag with format: ["nonce", "<nonce>", "<target_difficulty>"]
cJSON* nonce_tag = cJSON_CreateArray();
if (!nonce_tag) return -1;
char nonce_str[32];
char difficulty_str[16];
snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce);
snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty);
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str));
cJSON_AddItemToArray(tags, nonce_tag);
return 0;
}
/**
* Helper function to replace event content with successful PoW result
*/
static void replace_event_content(cJSON* target_event, cJSON* source_event) {
// Remove old fields
cJSON_DeleteItemFromObject(target_event, "id");
cJSON_DeleteItemFromObject(target_event, "sig");
cJSON_DeleteItemFromObject(target_event, "tags");
cJSON_DeleteItemFromObject(target_event, "created_at");
// Copy new fields from successful event
cJSON* id = cJSON_GetObjectItem(source_event, "id");
cJSON* sig = cJSON_GetObjectItem(source_event, "sig");
cJSON* tags = cJSON_GetObjectItem(source_event, "tags");
cJSON* created_at = cJSON_GetObjectItem(source_event, "created_at");
if (id) cJSON_AddItemToObject(target_event, "id", cJSON_Duplicate(id, 1));
if (sig) cJSON_AddItemToObject(target_event, "sig", cJSON_Duplicate(sig, 1));
if (tags) cJSON_AddItemToObject(target_event, "tags", cJSON_Duplicate(tags, 1));
if (created_at) cJSON_AddItemToObject(target_event, "created_at", cJSON_Duplicate(created_at, 1));
}
/**
* Add NIP-13 Proof of Work to an event
*
* @param event The event to add proof of work to
* @param private_key The private key for re-signing the event
* @param target_difficulty Target number of leading zero bits (default: 4 if 0)
* @param max_attempts Maximum number of mining attempts (default: 10,000,000 if <= 0)
* @param progress_report_interval How often to call progress callback (default: 10,000 if <= 0)
* @param timestamp_update_interval How often to update timestamp (default: 10,000 if <= 0)
* @param progress_callback Optional callback for mining progress
* @param user_data User data for progress callback
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty, int max_attempts,
int progress_report_interval, int timestamp_update_interval,
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
void* user_data) {
if (!event || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Set default difficulty if not specified (but allow 0 to disable PoW)
if (target_difficulty < 0) {
target_difficulty = 4;
}
// If target_difficulty is 0, skip proof of work entirely
if (target_difficulty == 0) {
return NOSTR_SUCCESS;
}
// Set default values for parameters
if (max_attempts <= 0) {
max_attempts = 10000000; // 10 million default
}
if (progress_report_interval <= 0) {
progress_report_interval = 10000; // Every 10,000 attempts
}
if (timestamp_update_interval <= 0) {
timestamp_update_interval = 10000; // Every 10,000 attempts
}
// Extract event data for reconstruction
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
if (!kind_item || !content_item || !created_at_item || !tags_item) {
return NOSTR_ERROR_INVALID_INPUT;
}
int kind = (int)cJSON_GetNumberValue(kind_item);
const char* content = cJSON_GetStringValue(content_item);
time_t original_timestamp = (time_t)cJSON_GetNumberValue(created_at_item);
uint64_t nonce = 0;
int attempts = 0;
time_t current_timestamp = original_timestamp;
// PoW difficulty tracking variables
int best_difficulty_this_round = 0;
int best_difficulty_overall = 0;
// Mining loop
while (attempts < max_attempts) {
// Update timestamp based on timestamp_update_interval
if (attempts % timestamp_update_interval == 0) {
current_timestamp = time(NULL);
#ifdef ENABLE_DEBUG_LOGGING
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
"nip013",
"PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d",
attempts,
best_difficulty_this_round,
best_difficulty_overall,
target_difficulty);
#endif
// Reset best difficulty for the new round
best_difficulty_this_round = 0;
}
// Call progress callback at specified intervals
if (progress_callback && (attempts % progress_report_interval == 0)) {
progress_callback(best_difficulty_overall, nonce, user_data);
}
// Create working copy of tags and add nonce
cJSON* working_tags = cJSON_Duplicate(tags_item, 1);
if (!working_tags) {
return NOSTR_ERROR_MEMORY_FAILED;
}
if (update_nonce_tag_with_difficulty(working_tags, nonce, target_difficulty) != 0) {
cJSON_Delete(working_tags);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Create and sign new event with current nonce
cJSON* test_event = nostr_create_and_sign_event(kind, content, working_tags,
private_key, current_timestamp);
cJSON_Delete(working_tags);
if (!test_event) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Check PoW difficulty
cJSON* id_item = cJSON_GetObjectItem(test_event, "id");
if (!id_item || !cJSON_IsString(id_item)) {
cJSON_Delete(test_event);
return NOSTR_ERROR_CRYPTO_FAILED;
}
const char* event_id = cJSON_GetStringValue(id_item);
unsigned char hash[32];
if (nostr_hex_to_bytes(event_id, hash, 32) != NOSTR_SUCCESS) {
cJSON_Delete(test_event);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Count leading zero bits using NIP-13 method
int current_difficulty = count_leading_zero_bits(hash);
// Update difficulty tracking
if (current_difficulty > best_difficulty_this_round) {
best_difficulty_this_round = current_difficulty;
}
if (current_difficulty > best_difficulty_overall) {
best_difficulty_overall = current_difficulty;
}
// Check if we've reached the target
if (current_difficulty >= target_difficulty) {
#ifdef ENABLE_DEBUG_LOGGING
nostr_log_emitf(NOSTR_LOG_LEVEL_INFO,
"nip013",
"PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts",
current_difficulty,
target_difficulty,
(unsigned long long)nonce,
attempts + 1);
// Print the final event JSON
char* event_json = cJSON_Print(test_event);
if (event_json) {
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
"nip013",
"Final event: %s",
event_json);
free(event_json);
}
#endif
// Copy successful result back to input event
replace_event_content(event, test_event);
cJSON_Delete(test_event);
return NOSTR_SUCCESS;
}
cJSON_Delete(test_event);
nonce++;
attempts++;
}
#ifdef ENABLE_DEBUG_LOGGING
// Debug logging - failure
nostr_log_emitf(NOSTR_LOG_LEVEL_WARN,
"nip013",
"PoW FAILED: Mining failed after %d attempts",
max_attempts);
#endif
// If we reach here, we've exceeded max attempts
return NOSTR_ERROR_CRYPTO_FAILED;
}
/**
* Calculate PoW difficulty (leading zero bits) for an event ID
*
* @param event_id_hex Hexadecimal event ID string (64 characters)
* @return Number of leading zero bits, or negative error code on failure
*/
int nostr_calculate_pow_difficulty(const char* event_id_hex) {
if (!event_id_hex) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate hex string length (should be 64 characters for SHA-256)
size_t hex_len = strlen(event_id_hex);
if (hex_len != 64) {
return NOSTR_ERROR_NIP13_CALCULATION;
}
// Convert hex to bytes
unsigned char hash[32];
if (nostr_hex_to_bytes(event_id_hex, hash, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_NIP13_CALCULATION;
}
// Use existing NIP-13 reference implementation
return count_leading_zero_bits(hash);
}
/**
* Extract nonce information from event tags
*
* @param event Complete event JSON object
* @param nonce_out Output pointer for nonce value (can be NULL)
* @param target_difficulty_out Output pointer for target difficulty (can be NULL)
* @return NOSTR_SUCCESS if nonce tag found, NOSTR_ERROR_NIP13_NO_NONCE_TAG if not found, other error codes on failure
*/
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize output values
if (nonce_out) *nonce_out = 0;
if (target_difficulty_out) *target_difficulty_out = -1;
// Get tags array
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
}
// Search for nonce tag
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
// Check if this is a nonce tag
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (!tag_type || !cJSON_IsString(tag_type)) {
continue;
}
const char* tag_name = cJSON_GetStringValue(tag_type);
if (!tag_name || strcmp(tag_name, "nonce") != 0) {
continue;
}
// Extract nonce value (second element)
cJSON* nonce_item = cJSON_GetArrayItem(tag, 1);
if (!nonce_item || !cJSON_IsString(nonce_item)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
const char* nonce_str = cJSON_GetStringValue(nonce_item);
if (!nonce_str) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Parse nonce value
char* endptr;
uint64_t nonce_val = strtoull(nonce_str, &endptr, 10);
if (*endptr != '\0') {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
if (nonce_out) *nonce_out = nonce_val;
// Extract target difficulty (third element, optional)
if (cJSON_GetArraySize(tag) >= 3) {
cJSON* target_item = cJSON_GetArrayItem(tag, 2);
if (target_item && cJSON_IsString(target_item)) {
const char* target_str = cJSON_GetStringValue(target_item);
if (target_str) {
char* endptr2;
long target_val = strtol(target_str, &endptr2, 10);
if (*endptr2 == '\0' && target_val >= 0) {
if (target_difficulty_out) *target_difficulty_out = (int)target_val;
}
}
}
}
return NOSTR_SUCCESS;
}
// No nonce tag found
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
}
/**
* Validate just the nonce tag format (without PoW calculation)
*
* @param nonce_tag_array JSON array representing a nonce tag
* @return NOSTR_SUCCESS if valid format, error code otherwise
*/
int nostr_validate_nonce_tag(cJSON* nonce_tag_array) {
if (!nonce_tag_array || !cJSON_IsArray(nonce_tag_array)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
int array_size = cJSON_GetArraySize(nonce_tag_array);
if (array_size < 2) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// First element should be "nonce"
cJSON* tag_type = cJSON_GetArrayItem(nonce_tag_array, 0);
if (!tag_type || !cJSON_IsString(tag_type)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
const char* tag_name = cJSON_GetStringValue(tag_type);
if (!tag_name || strcmp(tag_name, "nonce") != 0) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Second element should be a valid nonce (numeric string)
cJSON* nonce_item = cJSON_GetArrayItem(nonce_tag_array, 1);
if (!nonce_item || !cJSON_IsString(nonce_item)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
const char* nonce_str = cJSON_GetStringValue(nonce_item);
if (!nonce_str) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Validate nonce is a valid number
char* endptr;
strtoull(nonce_str, &endptr, 10);
if (*endptr != '\0') {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Third element (target difficulty) is optional, but if present should be valid
if (array_size >= 3) {
cJSON* target_item = cJSON_GetArrayItem(nonce_tag_array, 2);
if (target_item && cJSON_IsString(target_item)) {
const char* target_str = cJSON_GetStringValue(target_item);
if (target_str) {
char* endptr2;
strtol(target_str, &endptr2, 10);
if (*endptr2 != '\0') {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
}
}
}
return NOSTR_SUCCESS;
}
/**
* Validate Proof of Work for an event according to NIP-13
*
* @param event Complete event JSON object to validate
* @param min_difficulty Minimum difficulty required by the relay (0 = no requirement)
* @param validation_flags Bitflags for validation options
* @param result_info Optional output struct for detailed results (can be NULL)
* @return NOSTR_SUCCESS if PoW is valid and meets requirements, error code otherwise
*/
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
nostr_pow_result_t* result_info) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize result info if provided
if (result_info) {
result_info->actual_difficulty = 0;
result_info->committed_target = -1;
result_info->nonce_value = 0;
result_info->has_nonce_tag = 0;
result_info->error_detail[0] = '\0';
}
// Get event ID for PoW calculation
cJSON* id_item = cJSON_GetObjectItem(event, "id");
if (!id_item || !cJSON_IsString(id_item)) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Missing or invalid event ID");
}
return NOSTR_ERROR_EVENT_INVALID_ID;
}
const char* event_id = cJSON_GetStringValue(id_item);
if (!event_id) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Event ID is not a string");
}
return NOSTR_ERROR_EVENT_INVALID_ID;
}
// Calculate actual PoW difficulty
int actual_difficulty = nostr_calculate_pow_difficulty(event_id);
if (actual_difficulty < 0) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Failed to calculate PoW difficulty");
}
return actual_difficulty; // Return the specific error from calculation
}
if (result_info) {
result_info->actual_difficulty = actual_difficulty;
}
// Check if minimum difficulty requirement is met
if (min_difficulty > 0 && actual_difficulty < min_difficulty) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Insufficient difficulty: %d < %d", actual_difficulty, min_difficulty);
}
return NOSTR_ERROR_NIP13_INSUFFICIENT;
}
// Extract nonce information if validation flags require it
uint64_t nonce_value = 0;
int committed_target = -1;
int nonce_extract_result = nostr_extract_nonce_info(event, &nonce_value, &committed_target);
if (result_info) {
result_info->nonce_value = nonce_value;
result_info->committed_target = committed_target;
result_info->has_nonce_tag = (nonce_extract_result == NOSTR_SUCCESS) ? 1 : 0;
}
// Validate nonce tag presence if required
if (validation_flags & NOSTR_POW_VALIDATE_NONCE_TAG) {
if (nonce_extract_result == NOSTR_ERROR_NIP13_NO_NONCE_TAG) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Missing required nonce tag");
}
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
} else if (nonce_extract_result != NOSTR_SUCCESS) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Invalid nonce tag format");
}
return nonce_extract_result;
}
// If strict format validation is enabled, validate the nonce tag structure
if (validation_flags & NOSTR_POW_STRICT_FORMAT) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (tag_type && cJSON_IsString(tag_type)) {
const char* tag_name = cJSON_GetStringValue(tag_type);
if (tag_name && strcmp(tag_name, "nonce") == 0) {
int validation_result = nostr_validate_nonce_tag(tag);
if (validation_result != NOSTR_SUCCESS) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Nonce tag failed strict format validation");
}
return validation_result;
}
break;
}
}
}
}
}
}
}
// Validate committed target difficulty if required
if (validation_flags & NOSTR_POW_VALIDATE_TARGET_COMMIT) {
if (nonce_extract_result == NOSTR_SUCCESS && committed_target == -1) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Missing committed target difficulty in nonce tag");
}
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Check for target/difficulty mismatch if rejecting lower targets
if (validation_flags & NOSTR_POW_REJECT_LOWER_TARGET) {
// According to NIP-13: "if you require 40 bits to reply to your thread and see
// a committed target of 30, you can safely reject it even if the note has 40 bits difficulty"
// This means we reject if committed_target < min_difficulty, not actual_difficulty
if (committed_target != -1 && min_difficulty > 0 && committed_target < min_difficulty) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Committed target (%d) is lower than required minimum (%d)",
committed_target, min_difficulty);
}
return NOSTR_ERROR_NIP13_TARGET_MISMATCH;
}
}
}
// All validations passed
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"PoW validation successful");
}
return NOSTR_SUCCESS;
}
+48
View File
@@ -0,0 +1,48 @@
/*
* NOSTR Core Library - NIP-013: Proof of Work
*/
#ifndef NIP013_H
#define NIP013_H
#include "nip001.h"
#include <stdint.h>
// PoW validation flags
#define NOSTR_POW_VALIDATE_NONCE_TAG 0x01 // Require and validate nonce tag format
#define NOSTR_POW_VALIDATE_TARGET_COMMIT 0x02 // Validate committed target difficulty
#define NOSTR_POW_REJECT_LOWER_TARGET 0x04 // Reject if committed target < actual difficulty
#define NOSTR_POW_STRICT_FORMAT 0x08 // Strict nonce tag format validation
// Convenience combinations
#define NOSTR_POW_VALIDATE_BASIC (NOSTR_POW_VALIDATE_NONCE_TAG)
#define NOSTR_POW_VALIDATE_FULL (NOSTR_POW_VALIDATE_NONCE_TAG | NOSTR_POW_VALIDATE_TARGET_COMMIT)
#define NOSTR_POW_VALIDATE_ANTI_SPAM (NOSTR_POW_VALIDATE_FULL | NOSTR_POW_REJECT_LOWER_TARGET)
// Result information structure (optional output)
typedef struct {
int actual_difficulty; // Calculated difficulty (leading zero bits)
int committed_target; // Target difficulty from nonce tag (-1 if none)
uint64_t nonce_value; // Nonce value from tag (0 if none)
int has_nonce_tag; // 1 if nonce tag present, 0 otherwise
char error_detail[256]; // Detailed error description
} nostr_pow_result_t;
// Function declarations
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty, int max_attempts,
int progress_report_interval, int timestamp_update_interval,
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
void* user_data);
// PoW validation functions
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
nostr_pow_result_t* result_info);
int nostr_calculate_pow_difficulty(const char* event_id_hex);
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out);
int nostr_validate_nonce_tag(cJSON* nonce_tag_array);
#endif // NIP013_H
+407
View File
@@ -0,0 +1,407 @@
/*
* NIP-17: Private Direct Messages Implementation
* https://github.com/nostr-protocol/nips/blob/master/17.md
*/
#define _GNU_SOURCE
#include "nip017.h"
#include "nip059.h"
#include "nip001.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
/**
* Create tags array for DM events
*/
static cJSON* create_dm_tags(const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url) {
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// Add "p" tags for each recipient
for (int i = 0; i < num_recipients; i++) {
cJSON* p_tag = cJSON_CreateArray();
if (!p_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkeys[i]));
// Add relay URL if provided (recommended)
if (reply_relay_url) {
cJSON_AddItemToArray(p_tag, cJSON_CreateString(reply_relay_url));
}
cJSON_AddItemToArray(tags, p_tag);
}
// Add subject tag if provided
if (subject && strlen(subject) > 0) {
cJSON* subject_tag = cJSON_CreateArray();
if (!subject_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(subject_tag, cJSON_CreateString("subject"));
cJSON_AddItemToArray(subject_tag, cJSON_CreateString(subject));
cJSON_AddItemToArray(tags, subject_tag);
}
// Add reply reference if provided
if (reply_to_event_id && strlen(reply_to_event_id) > 0) {
cJSON* e_tag = cJSON_CreateArray();
if (!e_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_to_event_id));
if (reply_relay_url) {
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_relay_url));
}
// For replies, add "reply" marker as per NIP-17
cJSON_AddItemToArray(e_tag, cJSON_CreateString("reply"));
cJSON_AddItemToArray(tags, e_tag);
}
return tags;
}
/**
* NIP-17: Create a chat message event (kind 14)
*/
cJSON* nostr_nip17_create_chat_event(const char* message,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex) {
if (!message || !recipient_pubkeys || num_recipients <= 0 || !sender_pubkey_hex) {
return NULL;
}
// Create tags
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
reply_to_event_id, reply_relay_url);
if (!tags) {
return NULL;
}
// Create the chat rumor (kind 14, unsigned)
cJSON* chat_event = nostr_nip59_create_rumor(14, message, tags, sender_pubkey_hex, 0);
cJSON_Delete(tags); // Tags are duplicated in create_rumor
return chat_event;
}
/**
* NIP-17: Create a file message event (kind 15)
*/
cJSON* nostr_nip17_create_file_event(const char* file_url,
const char* file_type,
const char* encryption_algorithm,
const char* decryption_key,
const char* decryption_nonce,
const char* file_hash,
const char* original_file_hash,
size_t file_size,
const char* dimensions,
const char* blurhash,
const char* thumbnail_url,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex) {
if (!file_url || !file_type || !encryption_algorithm || !decryption_key ||
!decryption_nonce || !file_hash || !recipient_pubkeys ||
num_recipients <= 0 || !sender_pubkey_hex) {
return NULL;
}
// Create base tags
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
reply_to_event_id, reply_relay_url);
if (!tags) {
return NULL;
}
// Add file-specific tags
cJSON* file_type_tag = cJSON_CreateArray();
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString("file-type"));
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString(file_type));
cJSON_AddItemToArray(tags, file_type_tag);
cJSON* encryption_tag = cJSON_CreateArray();
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString("encryption-algorithm"));
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString(encryption_algorithm));
cJSON_AddItemToArray(tags, encryption_tag);
cJSON* key_tag = cJSON_CreateArray();
cJSON_AddItemToArray(key_tag, cJSON_CreateString("decryption-key"));
cJSON_AddItemToArray(key_tag, cJSON_CreateString(decryption_key));
cJSON_AddItemToArray(tags, key_tag);
cJSON* nonce_tag = cJSON_CreateArray();
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("decryption-nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(decryption_nonce));
cJSON_AddItemToArray(tags, nonce_tag);
cJSON* x_tag = cJSON_CreateArray();
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
cJSON_AddItemToArray(x_tag, cJSON_CreateString(file_hash));
cJSON_AddItemToArray(tags, x_tag);
// Optional tags
if (original_file_hash && strlen(original_file_hash) > 0) {
cJSON* ox_tag = cJSON_CreateArray();
cJSON_AddItemToArray(ox_tag, cJSON_CreateString("ox"));
cJSON_AddItemToArray(ox_tag, cJSON_CreateString(original_file_hash));
cJSON_AddItemToArray(tags, ox_tag);
}
if (file_size > 0) {
char size_str[32];
snprintf(size_str, sizeof(size_str), "%zu", file_size);
cJSON* size_tag = cJSON_CreateArray();
cJSON_AddItemToArray(size_tag, cJSON_CreateString("size"));
cJSON_AddItemToArray(size_tag, cJSON_CreateString(size_str));
cJSON_AddItemToArray(tags, size_tag);
}
if (dimensions && strlen(dimensions) > 0) {
cJSON* dim_tag = cJSON_CreateArray();
cJSON_AddItemToArray(dim_tag, cJSON_CreateString("dim"));
cJSON_AddItemToArray(dim_tag, cJSON_CreateString(dimensions));
cJSON_AddItemToArray(tags, dim_tag);
}
if (blurhash && strlen(blurhash) > 0) {
cJSON* blurhash_tag = cJSON_CreateArray();
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString("blurhash"));
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString(blurhash));
cJSON_AddItemToArray(tags, blurhash_tag);
}
if (thumbnail_url && strlen(thumbnail_url) > 0) {
cJSON* thumb_tag = cJSON_CreateArray();
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString("thumb"));
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString(thumbnail_url));
cJSON_AddItemToArray(tags, thumb_tag);
}
// Create the file rumor (kind 15, unsigned)
cJSON* file_event = nostr_nip59_create_rumor(15, file_url, tags, sender_pubkey_hex, 0);
cJSON_Delete(tags); // Tags are duplicated in create_rumor
return file_event;
}
/**
* NIP-17: Create a relay list event (kind 10050)
*/
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) {
return NULL;
}
// Get public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NULL;
}
char pubkey_hex[65];
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
// Create tags with relay URLs
cJSON* tags = cJSON_CreateArray();
if (!tags) {
return NULL;
}
for (int i = 0; i < num_relays; i++) {
cJSON* relay_tag = cJSON_CreateArray();
if (!relay_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_urls[i]));
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
return relay_event;
}
/**
* NIP-17: Send a direct message to recipients
*/
int nostr_nip17_send_dm(cJSON* dm_event,
const char** recipient_pubkeys,
int num_recipients,
const unsigned char* sender_private_key,
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) {
return -1;
}
int created_wraps = 0;
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
}
// Create seal for this recipient
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec);
if (!seal) {
continue; // Skip if sealing fails
}
// 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
if (!gift_wrap) {
continue; // Skip if wrapping fails
}
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
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 (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);
if (sender_gift_wrap) {
gift_wraps_out[created_wraps++] = sender_gift_wrap;
}
}
}
}
return created_wraps;
}
/**
* NIP-17: Receive and decrypt a direct message
*/
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
const unsigned char* recipient_private_key) {
if (!gift_wrap || !recipient_private_key) {
return NULL;
}
// Unwrap the gift wrap to get the seal
cJSON* seal = nostr_nip59_unwrap_gift(gift_wrap, recipient_private_key);
if (!seal) {
return NULL;
}
// Get sender's public key from the seal
cJSON* 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) {
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
return rumor;
}
/**
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
*/
int nostr_nip17_extract_dm_relays(cJSON* relay_list_event,
char** relay_urls_out,
int max_relays) {
if (!relay_list_event || !relay_urls_out || max_relays <= 0) {
return -1;
}
// Check if this is a kind 10050 event
cJSON* kind_item = cJSON_GetObjectItem(relay_list_event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || cJSON_GetNumberValue(kind_item) != 10050) {
return -1;
}
// Get tags array
cJSON* tags_item = cJSON_GetObjectItem(relay_list_event, "tags");
if (!tags_item || !cJSON_IsArray(tags_item)) {
return -1;
}
int extracted = 0;
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) {
continue;
}
// Check if this is a "relay" tag
cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0);
cJSON* relay_url = cJSON_GetArrayItem(tag_item, 1);
if (cJSON_IsString(tag_name) && cJSON_IsString(relay_url) &&
strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) {
if (extracted < max_relays) {
relay_urls_out[extracted] = strdup(cJSON_GetStringValue(relay_url));
if (relay_urls_out[extracted]) {
extracted++;
}
}
}
}
return extracted;
}
+139
View File
@@ -0,0 +1,139 @@
/*
* NIP-17: Private Direct Messages
* https://github.com/nostr-protocol/nips/blob/master/17.md
*/
#ifndef NOSTR_NIP017_H
#define NOSTR_NIP017_H
#include <stddef.h>
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* NIP-17: Create a chat message event (kind 14)
*
* @param message Plain text message content
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param subject Optional conversation subject/title (can be NULL)
* @param reply_to_event_id Optional event ID this message replies to (can be NULL)
* @param reply_relay_url Optional relay URL for reply reference (can be NULL)
* @param sender_pubkey_hex Sender's public key in hex format
* @return cJSON object representing the unsigned chat event, or NULL on error
*/
cJSON* nostr_nip17_create_chat_event(const char* message,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex);
/**
* NIP-17: Create a file message event (kind 15)
*
* @param file_url URL of the encrypted file
* @param file_type MIME type of the original file (e.g., "image/jpeg")
* @param encryption_algorithm Encryption algorithm used ("aes-gcm")
* @param decryption_key Base64-encoded decryption key
* @param decryption_nonce Base64-encoded decryption nonce
* @param file_hash SHA-256 hash of the encrypted file (hex)
* @param original_file_hash SHA-256 hash of the original file before encryption (hex, optional)
* @param file_size Size of encrypted file in bytes (optional, 0 to skip)
* @param dimensions Image dimensions in "WxH" format (optional, NULL to skip)
* @param blurhash Blurhash for preview (optional, NULL to skip)
* @param thumbnail_url URL of encrypted thumbnail (optional, NULL to skip)
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param subject Optional conversation subject/title (can be NULL)
* @param reply_to_event_id Optional event ID this message replies to (can be NULL)
* @param reply_relay_url Optional relay URL for reply reference (can be NULL)
* @param sender_pubkey_hex Sender's public key in hex format
* @return cJSON object representing the unsigned file event, or NULL on error
*/
cJSON* nostr_nip17_create_file_event(const char* file_url,
const char* file_type,
const char* encryption_algorithm,
const char* decryption_key,
const char* decryption_nonce,
const char* file_hash,
const char* original_file_hash,
size_t file_size,
const char* dimensions,
const char* blurhash,
const char* thumbnail_url,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex);
/**
* NIP-17: Create a relay list event (kind 10050)
*
* @param relay_urls Array of relay URLs for DM delivery
* @param num_relays Number of relay URLs
* @param private_key Sender's private key for signing
* @return cJSON object representing the signed relay list event, or NULL on error
*/
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
int num_relays,
const unsigned char* private_key);
/**
* NIP-17: Send a direct message to recipients
*
* This function creates the appropriate rumor, seals it, gift wraps it,
* and returns the final gift wrap events ready for publishing.
*
* @param dm_event The unsigned DM event (kind 14 or 15)
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param sender_private_key 32-byte sender private key
* @param gift_wraps_out Array to store resulting gift wrap events (caller must free)
* @param max_gift_wraps Maximum number of gift wraps to create
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
* @return Number of gift wrap events created, or -1 on error
*/
int nostr_nip17_send_dm(cJSON* dm_event,
const char** recipient_pubkeys,
int num_recipients,
const unsigned char* sender_private_key,
cJSON** gift_wraps_out,
int max_gift_wraps,
long max_delay_sec);
/**
* NIP-17: Receive and decrypt a direct message
*
* This function unwraps a gift wrap, unseals the rumor, and returns the original DM event.
*
* @param gift_wrap The received gift wrap event (kind 1059)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the decrypted DM event, or NULL on error
*/
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
const unsigned char* recipient_private_key);
/**
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
*
* @param relay_list_event The kind 10050 event
* @param relay_urls_out Array to store extracted relay URLs (caller must free)
* @param max_relays Maximum number of relays to extract
* @return Number of relay URLs extracted, or -1 on error
*/
int nostr_nip17_extract_dm_relays(cJSON* relay_list_event,
char** relay_urls_out,
int max_relays);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP017_H
+265
View File
@@ -0,0 +1,265 @@
/*
* NOSTR Core Library - NIP-019: Bech32-encoded Entities
*/
#include "nip019.h"
#include "utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "../nostr_core/nostr_common.h"
#define BECH32_CONST 1
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
static const int8_t bech32_charset_rev[128] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
static uint32_t bech32_polymod_step(uint32_t pre) {
uint8_t b = pre >> 25;
return ((pre & 0x1FFFFFF) << 5) ^
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
(-((b >> 1) & 1) & 0x26508e6dUL) ^
(-((b >> 2) & 1) & 0x1ea119faUL) ^
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
(-((b >> 4) & 1) & 0x2a1462b3UL);
}
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
*outlen = 0;
while (inlen--) {
val = (val << inbits) | *(in++);
bits += inbits;
while (bits >= outbits) {
bits -= outbits;
out[(*outlen)++] = (val >> bits) & maxv;
}
}
if (pad) {
if (bits) {
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
}
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
return 0;
}
return 1;
}
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
uint32_t chk = 1;
size_t i, hrp_len = strlen(hrp);
for (i = 0; i < hrp_len; ++i) {
int ch = hrp[i];
if (ch < 33 || ch > 126) return 0;
if (ch >= 'A' && ch <= 'Z') return 0;
chk = bech32_polymod_step(chk) ^ (ch >> 5);
}
chk = bech32_polymod_step(chk);
for (i = 0; i < hrp_len; ++i) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
*(output++) = hrp[i];
}
*(output++) = '1';
for (i = 0; i < data_len; ++i) {
if (*data >> 5) return 0;
chk = bech32_polymod_step(chk) ^ (*data);
*(output++) = bech32_charset[*(data++)];
}
for (i = 0; i < 6; ++i) {
chk = bech32_polymod_step(chk);
}
chk ^= BECH32_CONST;
for (i = 0; i < 6; ++i) {
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
}
*output = 0;
return 1;
}
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
if (!input || !hrp || !data || !data_len) {
return 0;
}
size_t input_len = strlen(input);
size_t hrp_len = strlen(hrp);
if (input_len < hrp_len + 7) return 0;
if (strncmp(input, hrp, hrp_len) != 0) return 0;
if (input[hrp_len] != '1') return 0;
const char* data_part = input + hrp_len + 1;
size_t data_part_len = input_len - hrp_len - 1;
uint8_t values[256];
for (size_t i = 0; i < data_part_len; i++) {
unsigned char c = (unsigned char)data_part[i];
if (c >= 128) return 0;
int8_t val = bech32_charset_rev[c];
if (val == -1) return 0;
values[i] = (uint8_t)val;
}
if (data_part_len < 6) return 0;
uint32_t chk = 1;
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
}
chk = bech32_polymod_step(chk);
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
}
for (size_t i = 0; i < data_part_len; i++) {
chk = bech32_polymod_step(chk) ^ values[i];
}
if (chk != BECH32_CONST) return 0;
size_t payload_len = data_part_len - 6;
size_t decoded_len;
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
return 0;
}
*data_len = decoded_len;
return 1;
}
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output) {
if (!key || !hrp || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
uint8_t conv[64];
size_t conv_len;
if (!convert_bits(conv, &conv_len, 5, key, 32, 8, 1)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
if (!bech32_encode(output, hrp, conv, conv_len)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
nostr_input_type_t nostr_detect_input_type(const char* input) {
if (!input || strlen(input) == 0) {
return NOSTR_INPUT_UNKNOWN;
}
size_t len = strlen(input);
// Check for bech32 nsec
if (len > 5 && strncmp(input, "nsec1", 5) == 0) {
return NOSTR_INPUT_NSEC_BECH32;
}
// Check for hex nsec (64 characters, all hex)
if (len == 64) {
int is_hex = 1;
for (size_t i = 0; i < len; i++) {
if (!isxdigit(input[i])) {
is_hex = 0;
break;
}
}
if (is_hex) {
return NOSTR_INPUT_NSEC_HEX;
}
}
// Check for mnemonic (space-separated words)
int word_count = 0;
char temp[1024];
strncpy(temp, input, sizeof(temp) - 1);
temp[sizeof(temp) - 1] = '\0';
char* token = strtok(temp, " ");
while (token != NULL) {
word_count++;
token = strtok(NULL, " ");
}
// BIP39 mnemonics are typically 12, 18, or 24 words
if (word_count >= 12 && word_count <= 24) {
return NOSTR_INPUT_MNEMONIC;
}
return NOSTR_INPUT_UNKNOWN;
}
int nostr_decode_nsec(const char* input, unsigned char* private_key) {
if (!input || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
nostr_input_type_t type = nostr_detect_input_type(input);
if (type == NOSTR_INPUT_NSEC_HEX) {
if (nostr_hex_to_bytes(input, private_key, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else if (type == NOSTR_INPUT_NSEC_BECH32) {
size_t decoded_len;
if (!bech32_decode(input, "nsec", private_key, &decoded_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (decoded_len != 32) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else {
return NOSTR_ERROR_INVALID_INPUT;
}
// TODO: Add private key validation if crypto functions are available
return NOSTR_SUCCESS;
}
int nostr_decode_npub(const char* input, unsigned char* public_key) {
if (!input || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
nostr_input_type_t type = nostr_detect_input_type(input);
if (type == NOSTR_INPUT_NSEC_HEX) { // Actually public key hex
if (nostr_hex_to_bytes(input, public_key, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else if (strncmp(input, "npub1", 4) == 0) { // Bech32 npub
size_t decoded_len;
if (!bech32_decode(input, "npub", public_key, &decoded_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (decoded_len != 32) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else {
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* NOSTR Core Library - NIP-019: Bech32-encoded Entities
*/
#ifndef NIP019_H
#define NIP019_H
#include "nip001.h"
#include "nip006.h" // For nostr_input_type_t enum
// Function declarations
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output);
nostr_input_type_t nostr_detect_input_type(const char* input);
int nostr_decode_nsec(const char* input, unsigned char* private_key);
int nostr_decode_npub(const char* input, unsigned char* public_key);
#endif // NIP019_H
+855
View File
@@ -0,0 +1,855 @@
/*
* NOSTR Core Library - NIP-021: nostr: URI scheme
*/
#include "nip021.h"
#include "nip019.h" // For existing bech32 functions
#include "utils.h"
#include "nostr_common.h" // For error codes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "../cjson/cJSON.h"
// Forward declarations for internal parsing functions
static int parse_nprofile_data(const uint8_t* data, size_t data_len, nostr_nprofile_t* nprofile);
static int parse_nevent_data(const uint8_t* data, size_t data_len, nostr_nevent_t* nevent);
static int parse_naddr_data(const uint8_t* data, size_t data_len, nostr_naddr_t* naddr);
// Bech32 constants and functions (copied from nip019.c for internal use)
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
static const int8_t bech32_charset_rev[128] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
static uint32_t bech32_polymod_step(uint32_t pre) {
uint8_t b = pre >> 25;
return ((pre & 0x1FFFFFF) << 5) ^
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
(-((b >> 1) & 1) & 0x26508e6dUL) ^
(-((b >> 2) & 1) & 0x1ea119faUL) ^
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
(-((b >> 4) & 1) & 0x2a1462b3UL);
}
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
*outlen = 0;
while (inlen--) {
val = (val << inbits) | *(in++);
bits += inbits;
while (bits >= outbits) {
bits -= outbits;
out[(*outlen)++] = (val >> bits) & maxv;
}
}
if (pad) {
if (bits) {
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
}
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
return 0;
}
return 1;
}
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
uint32_t chk = 1;
size_t i, hrp_len = strlen(hrp);
for (i = 0; i < hrp_len; ++i) {
int ch = hrp[i];
if (ch < 33 || ch > 126) return 0;
if (ch >= 'A' && ch <= 'Z') return 0;
chk = bech32_polymod_step(chk) ^ (ch >> 5);
}
chk = bech32_polymod_step(chk);
for (i = 0; i < hrp_len; ++i) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
*(output++) = hrp[i];
}
*(output++) = '1';
for (i = 0; i < data_len; ++i) {
if (*data >> 5) return 0;
chk = bech32_polymod_step(chk) ^ (*data);
*(output++) = bech32_charset[*(data++)];
}
for (i = 0; i < 6; ++i) {
chk = bech32_polymod_step(chk);
}
chk ^= 1;
for (i = 0; i < 6; ++i) {
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
}
*output = 0;
return 1;
}
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
if (!input || !hrp || !data || !data_len) {
return 0;
}
size_t input_len = strlen(input);
size_t hrp_len = strlen(hrp);
if (input_len < hrp_len + 7) return 0;
if (strncmp(input, hrp, hrp_len) != 0) return 0;
if (input[hrp_len] != '1') return 0;
const char* data_part = input + hrp_len + 1;
size_t data_part_len = input_len - hrp_len - 1;
uint8_t values[256];
for (size_t i = 0; i < data_part_len; i++) {
unsigned char c = (unsigned char)data_part[i];
if (c >= 128) return 0;
int8_t val = bech32_charset_rev[c];
if (val == -1) return 0;
values[i] = (uint8_t)val;
}
if (data_part_len < 6) return 0;
uint32_t chk = 1;
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
}
chk = bech32_polymod_step(chk);
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
}
for (size_t i = 0; i < data_part_len; i++) {
chk = bech32_polymod_step(chk) ^ values[i];
}
if (chk != 1) return 0;
size_t payload_len = data_part_len - 6;
size_t decoded_len;
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
return 0;
}
*data_len = decoded_len;
return 1;
}
// TLV (Type-Length-Value) constants for structured data
#define TLV_SPECIAL 0
#define TLV_RELAY 1
#define TLV_AUTHOR 2
#define TLV_KIND 3
#define TLV_CREATED_AT 4
#define TLV_IDENTIFIER 5
// Forward declarations for internal functions
static int tlv_encode(const uint8_t* data, size_t data_len, uint8_t type, uint8_t** output, size_t* output_len);
static int encode_structured_bech32(const char* hrp, const uint8_t* data, size_t data_len, char* output, size_t output_size);
static int decode_structured_bech32(const char* input, const char* expected_hrp, uint8_t** data, size_t* data_len);
// Utility function to duplicate string array (removed - not used)
// Free string array
static void free_string_array(char** array, int count) {
if (!array) return;
for (int i = 0; i < count; i++) {
free(array[i]);
}
free(array);
}
// TLV encoding: Type (1 byte) + Length (1 byte) + Value
static int tlv_encode(const uint8_t* data, size_t data_len, uint8_t type, uint8_t** output, size_t* output_len) {
if (data_len > 255) return 0; // Length must fit in 1 byte
*output_len = 2 + data_len;
*output = malloc(*output_len);
if (!*output) return 0;
(*output)[0] = type;
(*output)[1] = (uint8_t)data_len;
memcpy(*output + 2, data, data_len);
return 1;
}
// TLV decoding (removed - not used)
// Encode structured data to bech32
static int encode_structured_bech32(const char* hrp, const uint8_t* data, size_t data_len, char* output, size_t output_size) {
// For simple cases like note (32 bytes), use the existing key encoding
if (strcmp(hrp, "note") == 0 && data_len == 32) {
return nostr_key_to_bech32(data, "note", output);
}
uint8_t conv[256];
size_t conv_len;
if (!convert_bits(conv, &conv_len, 5, data, data_len, 8, 1)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (!bech32_encode(output, hrp, conv, conv_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (strlen(output) >= output_size) {
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
// Decode structured bech32 data
static int decode_structured_bech32(const char* input, const char* expected_hrp, uint8_t** data, size_t* data_len) {
// bech32_decode already converts from 5-bit to 8-bit internally
*data = malloc(256); // Max size
if (!*data) return NOSTR_ERROR_MEMORY_FAILED;
if (!bech32_decode(input, expected_hrp, *data, data_len)) {
free(*data);
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
// Detect URI type from string
nostr_uri_type_t nostr_detect_uri_type(const char* uri) {
if (!uri) return NOSTR_URI_INVALID;
// Check for nostr: prefix
if (strncmp(uri, "nostr:", 6) != 0) {
return NOSTR_URI_INVALID;
}
const char* bech32_part = uri + 6;
// Check prefixes
if (strncmp(bech32_part, "npub1", 5) == 0) return NOSTR_URI_NPUB;
if (strncmp(bech32_part, "nsec1", 5) == 0) return NOSTR_URI_NSEC;
if (strncmp(bech32_part, "note1", 5) == 0) return NOSTR_URI_NOTE;
if (strncmp(bech32_part, "nprofile1", 9) == 0) return NOSTR_URI_NPROFILE;
if (strncmp(bech32_part, "nevent1", 7) == 0) return NOSTR_URI_NEVENT;
if (strncmp(bech32_part, "naddr1", 6) == 0) return NOSTR_URI_NADDR;
return NOSTR_URI_INVALID;
}
// Free URI result resources
void nostr_uri_result_free(nostr_uri_result_t* result) {
if (!result) return;
switch (result->type) {
case NOSTR_URI_NPROFILE:
free_string_array(result->data.nprofile.relays, result->data.nprofile.relay_count);
break;
case NOSTR_URI_NEVENT:
free_string_array(result->data.nevent.relays, result->data.nevent.relay_count);
free(result->data.nevent.author);
free(result->data.nevent.kind);
free(result->data.nevent.created_at);
break;
case NOSTR_URI_NADDR:
free(result->data.naddr.identifier);
free_string_array(result->data.naddr.relays, result->data.naddr.relay_count);
break;
default:
break;
}
}
// Main URI parsing function
int nostr_parse_uri(const char* uri, nostr_uri_result_t* result) {
if (!uri || !result) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(result, 0, sizeof(nostr_uri_result_t));
nostr_uri_type_t type = nostr_detect_uri_type(uri);
if (type == NOSTR_URI_INVALID) {
return NOSTR_ERROR_INVALID_INPUT;
}
const char* bech32_part = uri + 6; // Skip "nostr:"
result->type = type;
int ret;
switch (type) {
case NOSTR_URI_NPUB: {
ret = nostr_decode_npub(bech32_part, result->data.pubkey);
break;
}
case NOSTR_URI_NSEC: {
ret = nostr_decode_nsec(bech32_part, result->data.privkey);
break;
}
case NOSTR_URI_NOTE: {
// Note is similar to npub but with "note" prefix
uint8_t* decoded;
size_t decoded_len;
ret = decode_structured_bech32(bech32_part, "note", &decoded, &decoded_len);
if (ret == NOSTR_SUCCESS) {
if (decoded_len == 32) {
memcpy(result->data.event_id, decoded, 32);
} else {
ret = NOSTR_ERROR_INVALID_INPUT;
}
free(decoded);
}
break;
}
case NOSTR_URI_NPROFILE: {
uint8_t* decoded;
size_t decoded_len;
ret = decode_structured_bech32(bech32_part, "nprofile", &decoded, &decoded_len);
if (ret == NOSTR_SUCCESS) {
ret = parse_nprofile_data(decoded, decoded_len, &result->data.nprofile);
free(decoded);
}
break;
}
case NOSTR_URI_NEVENT: {
uint8_t* decoded;
size_t decoded_len;
ret = decode_structured_bech32(bech32_part, "nevent", &decoded, &decoded_len);
if (ret == NOSTR_SUCCESS) {
ret = parse_nevent_data(decoded, decoded_len, &result->data.nevent);
free(decoded);
}
break;
}
case NOSTR_URI_NADDR: {
uint8_t* decoded;
size_t decoded_len;
ret = decode_structured_bech32(bech32_part, "naddr", &decoded, &decoded_len);
if (ret == NOSTR_SUCCESS) {
ret = parse_naddr_data(decoded, decoded_len, &result->data.naddr);
free(decoded);
}
break;
}
default:
ret = NOSTR_ERROR_INVALID_INPUT;
break;
}
if (ret != NOSTR_SUCCESS) {
nostr_uri_result_free(result);
memset(result, 0, sizeof(nostr_uri_result_t));
}
return ret;
}
// Parse nprofile structured data
static int parse_nprofile_data(const uint8_t* data, size_t data_len, nostr_nprofile_t* nprofile) {
size_t offset = 0;
while (offset < data_len) {
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
uint8_t type = data[offset];
uint8_t length = data[offset + 1];
offset += 2;
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
switch (type) {
case TLV_SPECIAL: // pubkey
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
memcpy(nprofile->pubkey, data + offset, 32);
break;
case TLV_RELAY: // relay URL
{
char* relay = malloc(length + 1);
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(relay, data + offset, length);
relay[length] = '\0';
char** new_relays = realloc(nprofile->relays, (nprofile->relay_count + 1) * sizeof(char*));
if (!new_relays) {
free(relay);
return NOSTR_ERROR_MEMORY_FAILED;
}
nprofile->relays = new_relays;
nprofile->relays[nprofile->relay_count++] = relay;
}
break;
default:
// Ignore unknown types
break;
}
offset += length;
}
return NOSTR_SUCCESS;
}
// Parse nevent structured data
static int parse_nevent_data(const uint8_t* data, size_t data_len, nostr_nevent_t* nevent) {
size_t offset = 0;
while (offset < data_len) {
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
uint8_t type = data[offset];
uint8_t length = data[offset + 1];
offset += 2;
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
switch (type) {
case TLV_SPECIAL: // event ID
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
memcpy(nevent->event_id, data + offset, 32);
break;
case TLV_RELAY: // relay URL
{
char* relay = malloc(length + 1);
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(relay, data + offset, length);
relay[length] = '\0';
char** new_relays = realloc(nevent->relays, (nevent->relay_count + 1) * sizeof(char*));
if (!new_relays) {
free(relay);
return NOSTR_ERROR_MEMORY_FAILED;
}
nevent->relays = new_relays;
nevent->relays[nevent->relay_count++] = relay;
}
break;
case TLV_AUTHOR: // author pubkey
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
nevent->author = malloc(32);
if (!nevent->author) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(nevent->author, data + offset, 32);
break;
case TLV_KIND: // kind
if (length != 4) return NOSTR_ERROR_INVALID_INPUT;
nevent->kind = malloc(sizeof(int));
if (!nevent->kind) return NOSTR_ERROR_MEMORY_FAILED;
*nevent->kind = (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3];
break;
case TLV_CREATED_AT: // created_at
if (length != 8) return NOSTR_ERROR_INVALID_INPUT;
nevent->created_at = malloc(sizeof(time_t));
if (!nevent->created_at) return NOSTR_ERROR_MEMORY_FAILED;
*nevent->created_at = ((time_t)data[offset] << 56) | ((time_t)data[offset+1] << 48) |
((time_t)data[offset+2] << 40) | ((time_t)data[offset+3] << 32) |
((time_t)data[offset+4] << 24) | ((time_t)data[offset+5] << 16) |
((time_t)data[offset+6] << 8) | (time_t)data[offset+7];
break;
default:
// Ignore unknown types
break;
}
offset += length;
}
return NOSTR_SUCCESS;
}
// Parse naddr structured data
static int parse_naddr_data(const uint8_t* data, size_t data_len, nostr_naddr_t* naddr) {
size_t offset = 0;
while (offset < data_len) {
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
uint8_t type = data[offset];
uint8_t length = data[offset + 1];
offset += 2;
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
switch (type) {
case TLV_IDENTIFIER: // identifier
naddr->identifier = malloc(length + 1);
if (!naddr->identifier) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(naddr->identifier, data + offset, length);
naddr->identifier[length] = '\0';
break;
case TLV_SPECIAL: // pubkey
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
memcpy(naddr->pubkey, data + offset, 32);
break;
case TLV_KIND: // kind
if (length != 4) return NOSTR_ERROR_INVALID_INPUT;
naddr->kind = (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3];
break;
case TLV_RELAY: // relay URL
{
char* relay = malloc(length + 1);
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(relay, data + offset, length);
relay[length] = '\0';
char** new_relays = realloc(naddr->relays, (naddr->relay_count + 1) * sizeof(char*));
if (!new_relays) {
free(relay);
return NOSTR_ERROR_MEMORY_FAILED;
}
naddr->relays = new_relays;
naddr->relays[naddr->relay_count++] = relay;
}
break;
default:
// Ignore unknown types
break;
}
offset += length;
}
return NOSTR_SUCCESS;
}
// URI construction functions
int nostr_build_uri_npub(const unsigned char* pubkey, char* output, size_t output_size) {
if (!pubkey || !output || output_size < 70) {
return NOSTR_ERROR_INVALID_INPUT;
}
char bech32[100];
int ret = nostr_key_to_bech32(pubkey, "npub", bech32);
if (ret != NOSTR_SUCCESS) return ret;
size_t len = strlen(bech32);
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
strcpy(output, "nostr:");
strcpy(output + 6, bech32);
return NOSTR_SUCCESS;
}
int nostr_build_uri_nsec(const unsigned char* privkey, char* output, size_t output_size) {
if (!privkey || !output || output_size < 70) {
return NOSTR_ERROR_INVALID_INPUT;
}
char bech32[100];
int ret = nostr_key_to_bech32(privkey, "nsec", bech32);
if (ret != NOSTR_SUCCESS) return ret;
size_t len = strlen(bech32);
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
strcpy(output, "nostr:");
strcpy(output + 6, bech32);
return NOSTR_SUCCESS;
}
// Helper to build URI with prefix
static int build_uri_with_prefix(const char* bech32, char* output, size_t output_size) {
size_t len = strlen(bech32);
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
strcpy(output, "nostr:");
strcpy(output + 6, bech32);
return NOSTR_SUCCESS;
}
int nostr_build_uri_note(const unsigned char* event_id, char* output, size_t output_size) {
if (!event_id || !output || output_size < 70) {
return NOSTR_ERROR_INVALID_INPUT;
}
char bech32[100];
int ret = encode_structured_bech32("note", event_id, 32, bech32, sizeof(bech32));
if (ret != NOSTR_SUCCESS) return ret;
return build_uri_with_prefix(bech32, output, output_size);
}
int nostr_build_uri_nprofile(const unsigned char* pubkey, const char** relays, int relay_count,
char* output, size_t output_size) {
if (!pubkey || !output) return NOSTR_ERROR_INVALID_INPUT;
// Build TLV data
uint8_t* data = NULL;
size_t data_len = 0;
// Add pubkey (special)
uint8_t* pubkey_tlv;
size_t pubkey_tlv_len;
if (!tlv_encode(pubkey, 32, TLV_SPECIAL, &pubkey_tlv, &pubkey_tlv_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + pubkey_tlv_len);
if (!data) {
free(pubkey_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, pubkey_tlv, pubkey_tlv_len);
data_len += pubkey_tlv_len;
free(pubkey_tlv);
// Add relays
for (int i = 0; i < relay_count; i++) {
size_t relay_len = strlen(relays[i]);
uint8_t* relay_tlv;
size_t relay_tlv_len;
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + relay_tlv_len);
if (!data) {
free(relay_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, relay_tlv, relay_tlv_len);
data_len += relay_tlv_len;
free(relay_tlv);
}
// Encode to bech32
char bech32[500];
int ret = encode_structured_bech32("nprofile", data, data_len, bech32, sizeof(bech32));
free(data);
if (ret != NOSTR_SUCCESS) return ret;
return build_uri_with_prefix(bech32, output, output_size);
}
int nostr_build_uri_nevent(const unsigned char* event_id, const char** relays, int relay_count,
const unsigned char* author, int kind, time_t created_at,
char* output, size_t output_size) {
if (!event_id || !output) return NOSTR_ERROR_INVALID_INPUT;
// Build TLV data
uint8_t* data = NULL;
size_t data_len = 0;
// Add event_id (special)
uint8_t* event_tlv;
size_t event_tlv_len;
if (!tlv_encode(event_id, 32, TLV_SPECIAL, &event_tlv, &event_tlv_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + event_tlv_len);
if (!data) {
free(event_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, event_tlv, event_tlv_len);
data_len += event_tlv_len;
free(event_tlv);
// Add relays
for (int i = 0; i < relay_count; i++) {
size_t relay_len = strlen(relays[i]);
uint8_t* relay_tlv;
size_t relay_tlv_len;
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + relay_tlv_len);
if (!data) {
free(relay_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, relay_tlv, relay_tlv_len);
data_len += relay_tlv_len;
free(relay_tlv);
}
// Add author if provided
if (author) {
uint8_t* author_tlv;
size_t author_tlv_len;
if (!tlv_encode(author, 32, TLV_AUTHOR, &author_tlv, &author_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + author_tlv_len);
if (!data) {
free(author_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, author_tlv, author_tlv_len);
data_len += author_tlv_len;
free(author_tlv);
}
// Add kind if provided
if (kind >= 0) {
uint8_t kind_bytes[4];
kind_bytes[0] = (kind >> 24) & 0xFF;
kind_bytes[1] = (kind >> 16) & 0xFF;
kind_bytes[2] = (kind >> 8) & 0xFF;
kind_bytes[3] = kind & 0xFF;
uint8_t* kind_tlv;
size_t kind_tlv_len;
if (!tlv_encode(kind_bytes, 4, TLV_KIND, &kind_tlv, &kind_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + kind_tlv_len);
if (!data) {
free(kind_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, kind_tlv, kind_tlv_len);
data_len += kind_tlv_len;
free(kind_tlv);
}
// Add created_at if provided
if (created_at > 0) {
uint8_t time_bytes[8];
time_bytes[0] = (created_at >> 56) & 0xFF;
time_bytes[1] = (created_at >> 48) & 0xFF;
time_bytes[2] = (created_at >> 40) & 0xFF;
time_bytes[3] = (created_at >> 32) & 0xFF;
time_bytes[4] = (created_at >> 24) & 0xFF;
time_bytes[5] = (created_at >> 16) & 0xFF;
time_bytes[6] = (created_at >> 8) & 0xFF;
time_bytes[7] = created_at & 0xFF;
uint8_t* time_tlv;
size_t time_tlv_len;
if (!tlv_encode(time_bytes, 8, TLV_CREATED_AT, &time_tlv, &time_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + time_tlv_len);
if (!data) {
free(time_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, time_tlv, time_tlv_len);
data_len += time_tlv_len;
free(time_tlv);
}
// Encode to bech32
char bech32[1000];
int ret = encode_structured_bech32("nevent", data, data_len, bech32, sizeof(bech32));
free(data);
if (ret != NOSTR_SUCCESS) return ret;
return build_uri_with_prefix(bech32, output, output_size);
}
int nostr_build_uri_naddr(const char* identifier, const unsigned char* pubkey, int kind,
const char** relays, int relay_count, char* output, size_t output_size) {
if (!identifier || !pubkey || !output) return NOSTR_ERROR_INVALID_INPUT;
// Build TLV data
uint8_t* data = NULL;
size_t data_len = 0;
// Add identifier
size_t id_len = strlen(identifier);
uint8_t* id_tlv;
size_t id_tlv_len;
if (!tlv_encode((uint8_t*)identifier, id_len, TLV_IDENTIFIER, &id_tlv, &id_tlv_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + id_tlv_len);
if (!data) {
free(id_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, id_tlv, id_tlv_len);
data_len += id_tlv_len;
free(id_tlv);
// Add pubkey (special)
uint8_t* pubkey_tlv;
size_t pubkey_tlv_len;
if (!tlv_encode(pubkey, 32, TLV_SPECIAL, &pubkey_tlv, &pubkey_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + pubkey_tlv_len);
if (!data) {
free(pubkey_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, pubkey_tlv, pubkey_tlv_len);
data_len += pubkey_tlv_len;
free(pubkey_tlv);
// Add kind
uint8_t kind_bytes[4];
kind_bytes[0] = (kind >> 24) & 0xFF;
kind_bytes[1] = (kind >> 16) & 0xFF;
kind_bytes[2] = (kind >> 8) & 0xFF;
kind_bytes[3] = kind & 0xFF;
uint8_t* kind_tlv;
size_t kind_tlv_len;
if (!tlv_encode(kind_bytes, 4, TLV_KIND, &kind_tlv, &kind_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + kind_tlv_len);
if (!data) {
free(kind_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, kind_tlv, kind_tlv_len);
data_len += kind_tlv_len;
free(kind_tlv);
// Add relays
for (int i = 0; i < relay_count; i++) {
size_t relay_len = strlen(relays[i]);
uint8_t* relay_tlv;
size_t relay_tlv_len;
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
free(data);
return NOSTR_ERROR_INVALID_INPUT;
}
data = realloc(data, data_len + relay_tlv_len);
if (!data) {
free(relay_tlv);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(data + data_len, relay_tlv, relay_tlv_len);
data_len += relay_tlv_len;
free(relay_tlv);
}
// Encode to bech32
char bech32[1000];
int ret = encode_structured_bech32("naddr", data, data_len, bech32, sizeof(bech32));
free(data);
if (ret != NOSTR_SUCCESS) return ret;
return build_uri_with_prefix(bech32, output, output_size);
}
+81
View File
@@ -0,0 +1,81 @@
/*
* NOSTR Core Library - NIP-021: nostr: URI scheme
*/
#ifndef NIP021_H
#define NIP021_H
#include <stdint.h>
#include <time.h>
#include "nip001.h"
// URI type enumeration
typedef enum {
NOSTR_URI_NPUB, // Simple 32-byte pubkey
NOSTR_URI_NSEC, // Simple 32-byte privkey
NOSTR_URI_NOTE, // Simple 32-byte event ID
NOSTR_URI_NPROFILE, // Structured: pubkey + relays
NOSTR_URI_NEVENT, // Structured: event ID + relays + metadata
NOSTR_URI_NADDR, // Structured: address + relays + metadata
NOSTR_URI_INVALID
} nostr_uri_type_t;
// Structured data types for complex URIs
typedef struct {
unsigned char pubkey[32];
char** relays;
int relay_count;
} nostr_nprofile_t;
typedef struct {
unsigned char event_id[32];
char** relays;
int relay_count;
unsigned char* author; // Optional, 32 bytes if present
int* kind; // Optional
time_t* created_at; // Optional
} nostr_nevent_t;
typedef struct {
char* identifier;
unsigned char pubkey[32];
int kind;
char** relays;
int relay_count;
} nostr_naddr_t;
// Unified URI result structure
typedef struct {
nostr_uri_type_t type;
union {
unsigned char pubkey[32]; // For NPUB
unsigned char privkey[32]; // For NSEC
unsigned char event_id[32]; // For NOTE
nostr_nprofile_t nprofile; // For NPROFILE
nostr_nevent_t nevent; // For NEVENT
nostr_naddr_t naddr; // For NADDR
} data;
} nostr_uri_result_t;
// Function declarations
// Main parsing function - unified entry point
int nostr_parse_uri(const char* uri, nostr_uri_result_t* result);
// URI construction functions
int nostr_build_uri_npub(const unsigned char* pubkey, char* output, size_t output_size);
int nostr_build_uri_nsec(const unsigned char* privkey, char* output, size_t output_size);
int nostr_build_uri_note(const unsigned char* event_id, char* output, size_t output_size);
int nostr_build_uri_nprofile(const unsigned char* pubkey, const char** relays, int relay_count,
char* output, size_t output_size);
int nostr_build_uri_nevent(const unsigned char* event_id, const char** relays, int relay_count,
const unsigned char* author, int kind, time_t created_at,
char* output, size_t output_size);
int nostr_build_uri_naddr(const char* identifier, const unsigned char* pubkey, int kind,
const char** relays, int relay_count, char* output, size_t output_size);
// Utility functions
void nostr_uri_result_free(nostr_uri_result_t* result);
nostr_uri_type_t nostr_detect_uri_type(const char* uri);
#endif // NIP021_H
+628
View File
@@ -0,0 +1,628 @@
/*
* NOSTR Core Library - NIP-042: Authentication of clients to relays
*
* Implements client authentication through signed ephemeral events
*/
#include "nip042.h"
#include "nip001.h"
#include "utils.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// =============================================================================
// CLIENT-SIDE FUNCTIONS
// =============================================================================
/**
* 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) {
return NULL;
}
// Validate challenge format
size_t challenge_len = strlen(challenge);
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
return NULL;
}
// Create tags array with relay and challenge
cJSON* tags = cJSON_CreateArray();
if (!tags) {
return NULL;
}
// Add relay tag
cJSON* relay_tag = cJSON_CreateArray();
if (!relay_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_url));
cJSON_AddItemToArray(tags, relay_tag);
// Add challenge tag
cJSON* challenge_tag = cJSON_CreateArray();
if (!challenge_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString("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(
NOSTR_NIP42_AUTH_EVENT_KIND,
"", // Empty content
tags,
private_key,
timestamp
);
cJSON_Delete(tags);
return auth_event;
}
/**
* Create AUTH message JSON for relay communication
*/
char* nostr_nip42_create_auth_message(cJSON* auth_event) {
if (!auth_event) {
return NULL;
}
// Create AUTH message array: ["AUTH", <event-json>]
cJSON* message_array = cJSON_CreateArray();
if (!message_array) {
return NULL;
}
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
cJSON_AddItemToArray(message_array, cJSON_Duplicate(auth_event, 1));
char* message_string = cJSON_PrintUnformatted(message_array);
cJSON_Delete(message_array);
return message_string;
}
/**
* Validate challenge string format and freshness
*/
int nostr_nip42_validate_challenge(const char* challenge,
time_t received_at,
int time_tolerance) {
if (!challenge) {
return NOSTR_ERROR_INVALID_INPUT;
}
size_t challenge_len = strlen(challenge);
// Check challenge length
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH) {
return NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT;
}
if (challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
return NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG;
}
// Check time validity if provided
if (received_at > 0) {
time_t now = time(NULL);
int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
if (now - received_at > tolerance) {
return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED;
}
}
return NOSTR_SUCCESS;
}
/**
* Parse AUTH challenge message from relay
*/
int nostr_nip42_parse_auth_challenge(const char* message,
char* challenge_out,
size_t challenge_size) {
if (!message || !challenge_out || challenge_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* json = cJSON_Parse(message);
if (!json || !cJSON_IsArray(json)) {
if (json) cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check array has exactly 2 elements
if (cJSON_GetArraySize(json) != 2) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check first element is "AUTH"
cJSON* message_type = cJSON_GetArrayItem(json, 0);
if (!message_type || !cJSON_IsString(message_type) ||
strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Get challenge string
cJSON* challenge_item = cJSON_GetArrayItem(json, 1);
if (!challenge_item || !cJSON_IsString(challenge_item)) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
const char* challenge_str = cJSON_GetStringValue(challenge_item);
if (!challenge_str || strlen(challenge_str) >= challenge_size) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
}
strcpy(challenge_out, challenge_str);
cJSON_Delete(json);
return NOSTR_SUCCESS;
}
// =============================================================================
// SERVER-SIDE FUNCTIONS
// =============================================================================
/**
* Generate cryptographically secure challenge string
*/
int nostr_nip42_generate_challenge(char* challenge_out, size_t length) {
if (!challenge_out || length < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
length > NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate random bytes
unsigned char random_bytes[NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2];
if (nostr_secp256k1_get_random_bytes(random_bytes, length) != 1) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Convert to hex string (reusing existing function)
nostr_bytes_to_hex(random_bytes, length, challenge_out);
return NOSTR_SUCCESS;
}
/**
* Verify NIP-42 authentication event
*/
int nostr_nip42_verify_auth_event(cJSON* auth_event,
const char* expected_challenge,
const char* relay_url,
int time_tolerance) {
if (!auth_event || !expected_challenge || !relay_url) {
return NOSTR_ERROR_INVALID_INPUT;
}
// First validate basic event structure using existing function
int structure_result = nostr_validate_event_structure(auth_event);
if (structure_result != NOSTR_SUCCESS) {
return structure_result;
}
// Validate NIP-42 specific structure
int nip42_structure_result = nostr_nip42_validate_auth_event_structure(
auth_event, relay_url, expected_challenge, time_tolerance);
if (nip42_structure_result != NOSTR_SUCCESS) {
return nip42_structure_result;
}
// Finally verify cryptographic signature using existing function
return nostr_verify_event_signature(auth_event);
}
/**
* Parse AUTH message from client
*/
int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out) {
if (!message || !auth_event_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* json = cJSON_Parse(message);
if (!json || !cJSON_IsArray(json)) {
if (json) cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check array has exactly 2 elements
if (cJSON_GetArraySize(json) != 2) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check first element is "AUTH"
cJSON* message_type = cJSON_GetArrayItem(json, 0);
if (!message_type || !cJSON_IsString(message_type) ||
strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Get event object
cJSON* event_item = cJSON_GetArrayItem(json, 1);
if (!event_item || !cJSON_IsObject(event_item)) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Duplicate the event for the caller
*auth_event_out = cJSON_Duplicate(event_item, 1);
cJSON_Delete(json);
if (!*auth_event_out) {
return NOSTR_ERROR_MEMORY_FAILED;
}
return NOSTR_SUCCESS;
}
/**
* Create "auth-required" error response
*/
char* nostr_nip42_create_auth_required_message(const char* subscription_id,
const char* event_id,
const char* reason) {
const char* default_reason = "authentication required";
const char* message_reason = reason ? reason : default_reason;
cJSON* response = cJSON_CreateArray();
if (!response) {
return NULL;
}
if (subscription_id) {
// CLOSED message for subscriptions
cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id));
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else if (event_id) {
// OK message for events
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else {
cJSON_Delete(response);
return NULL;
}
char* message_string = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
return message_string;
}
/**
* Create "restricted" error response
*/
char* nostr_nip42_create_restricted_message(const char* subscription_id,
const char* event_id,
const char* reason) {
const char* default_reason = "access restricted";
const char* message_reason = reason ? reason : default_reason;
cJSON* response = cJSON_CreateArray();
if (!response) {
return NULL;
}
if (subscription_id) {
// CLOSED message for subscriptions
cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id));
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else if (event_id) {
// OK message for events
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else {
cJSON_Delete(response);
return NULL;
}
char* message_string = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
return message_string;
}
// =============================================================================
// URL NORMALIZATION FUNCTIONS
// =============================================================================
/**
* Normalize relay URL for comparison
*/
char* nostr_nip42_normalize_url(const char* url) {
if (!url) {
return NULL;
}
size_t url_len = strlen(url);
char* normalized = malloc(url_len + 1);
if (!normalized) {
return NULL;
}
strcpy(normalized, url);
// Remove trailing slash
if (url_len > 1 && normalized[url_len - 1] == '/') {
normalized[url_len - 1] = '\0';
}
// Convert to lowercase for domain comparison
for (size_t i = 0; normalized[i]; i++) {
if (normalized[i] >= 'A' && normalized[i] <= 'Z') {
normalized[i] = normalized[i] + ('a' - 'A');
}
}
return normalized;
}
/**
* Check if two relay URLs match after normalization
*/
int nostr_nip42_urls_match(const char* url1, const char* url2) {
if (!url1 || !url2) {
return -1;
}
char* norm1 = nostr_nip42_normalize_url(url1);
char* norm2 = nostr_nip42_normalize_url(url2);
if (!norm1 || !norm2) {
free(norm1);
free(norm2);
return -1;
}
int result = (strcmp(norm1, norm2) == 0) ? 1 : 0;
free(norm1);
free(norm2);
return result;
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Get string description of authentication state
*/
const char* nostr_nip42_auth_state_str(nostr_auth_state_t state) {
switch (state) {
case NOSTR_AUTH_STATE_NONE:
return "none";
case NOSTR_AUTH_STATE_CHALLENGE_RECEIVED:
return "challenge_received";
case NOSTR_AUTH_STATE_AUTHENTICATING:
return "authenticating";
case NOSTR_AUTH_STATE_AUTHENTICATED:
return "authenticated";
case NOSTR_AUTH_STATE_REJECTED:
return "rejected";
default:
return "unknown";
}
}
/**
* Initialize authentication context structure
*/
int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx,
const char* relay_url,
const char* challenge,
int time_tolerance) {
if (!ctx || !relay_url || !challenge) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(ctx, 0, sizeof(nostr_auth_context_t));
ctx->relay_url = malloc(strlen(relay_url) + 1);
if (!ctx->relay_url) {
return NOSTR_ERROR_MEMORY_FAILED;
}
strcpy(ctx->relay_url, relay_url);
ctx->challenge = malloc(strlen(challenge) + 1);
if (!ctx->challenge) {
free(ctx->relay_url);
ctx->relay_url = NULL;
return NOSTR_ERROR_MEMORY_FAILED;
}
strcpy(ctx->challenge, challenge);
ctx->timestamp = time(NULL);
ctx->time_tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
return NOSTR_SUCCESS;
}
/**
* Free authentication context structure
*/
void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx) {
if (!ctx) {
return;
}
free(ctx->relay_url);
free(ctx->challenge);
free(ctx->pubkey_hex);
memset(ctx, 0, sizeof(nostr_auth_context_t));
}
/**
* Validate authentication event structure (without signature verification)
*/
int nostr_nip42_validate_auth_event_structure(cJSON* auth_event,
const char* relay_url,
const char* challenge,
int time_tolerance) {
if (!auth_event || !relay_url || !challenge) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check event kind is 22242
cJSON* kind_item = cJSON_GetObjectItem(auth_event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) ||
(int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP42_AUTH_EVENT_KIND) {
return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID;
}
// Check timestamp is within tolerance
cJSON* created_at_item = cJSON_GetObjectItem(auth_event, "created_at");
if (!created_at_item || !cJSON_IsNumber(created_at_item)) {
return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
}
time_t event_time = (time_t)cJSON_GetNumberValue(created_at_item);
time_t now = time(NULL);
int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
if (abs((int)(now - event_time)) > tolerance) {
return NOSTR_ERROR_NIP42_TIME_TOLERANCE;
}
// Check tags contain required relay and challenge
cJSON* tags_item = cJSON_GetObjectItem(auth_event, "tags");
if (!tags_item || !cJSON_IsArray(tags_item)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
int found_relay = 0, found_challenge = 0;
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) {
continue;
}
cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag_item, 1);
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
continue;
}
const char* name = cJSON_GetStringValue(tag_name);
const char* value = cJSON_GetStringValue(tag_value);
if (strcmp(name, "relay") == 0) {
if (nostr_nip42_urls_match(value, relay_url) == 1) {
found_relay = 1;
}
} else if (strcmp(name, "challenge") == 0) {
if (strcmp(value, challenge) == 0) {
found_challenge = 1;
}
}
}
if (!found_relay) {
return NOSTR_ERROR_NIP42_URL_MISMATCH;
}
if (!found_challenge) {
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
}
return NOSTR_SUCCESS;
}
// =============================================================================
// WEBSOCKET CLIENT INTEGRATION STUB FUNCTIONS
// =============================================================================
// Note: These will need to be implemented when WebSocket client structure is available
int nostr_ws_authenticate(struct nostr_ws_client* client,
const unsigned char* private_key,
int time_tolerance) {
// TODO: Implement when WebSocket client structure is available
(void)client;
(void)private_key;
(void)time_tolerance;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client) {
// TODO: Implement when WebSocket client structure is available
(void)client;
return NOSTR_AUTH_STATE_NONE; // Placeholder
}
int nostr_ws_has_valid_challenge(struct nostr_ws_client* client) {
// TODO: Implement when WebSocket client structure is available
(void)client;
return 0; // Placeholder
}
int nostr_ws_get_challenge(struct nostr_ws_client* client,
char* challenge_out,
size_t challenge_size) {
// TODO: Implement when WebSocket client structure is available
(void)client;
(void)challenge_out;
(void)challenge_size;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
int nostr_ws_store_challenge(struct nostr_ws_client* client,
const char* challenge) {
// TODO: Implement when WebSocket client structure is available
(void)client;
(void)challenge;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
int nostr_ws_clear_auth_state(struct nostr_ws_client* client) {
// TODO: Implement when WebSocket client structure is available
(void)client;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
+281
View File
@@ -0,0 +1,281 @@
/*
* NOSTR Core Library - NIP-042: Authentication of clients to relays
*
* Implements client authentication through signed ephemeral events
*/
#ifndef NIP042_H
#define NIP042_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "../cjson/cJSON.h"
#include "nostr_common.h"
#ifdef __cplusplus
extern "C" {
#endif
// =============================================================================
// NIP-42 CONSTANTS AND DEFINITIONS
// =============================================================================
#define NOSTR_NIP42_AUTH_EVENT_KIND 22242
#define NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH 32
#define NOSTR_NIP42_DEFAULT_TIME_TOLERANCE 600 // 10 minutes in seconds
#define NOSTR_NIP42_MAX_CHALLENGE_LENGTH 256
#define NOSTR_NIP42_MIN_CHALLENGE_LENGTH 16
// Authentication states for WebSocket client integration
typedef enum {
NOSTR_AUTH_STATE_NONE = 0, // No authentication attempted
NOSTR_AUTH_STATE_CHALLENGE_RECEIVED = 1, // Challenge received from relay
NOSTR_AUTH_STATE_AUTHENTICATING = 2, // AUTH event sent, waiting for OK
NOSTR_AUTH_STATE_AUTHENTICATED = 3, // Successfully authenticated
NOSTR_AUTH_STATE_REJECTED = 4 // Authentication rejected
} nostr_auth_state_t;
// Challenge storage structure
typedef struct {
char challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
time_t received_at;
int is_valid;
} nostr_auth_challenge_t;
// Authentication context for relay verification
typedef struct {
char* relay_url;
char* challenge;
time_t timestamp;
int time_tolerance;
char* pubkey_hex;
} nostr_auth_context_t;
// =============================================================================
// CLIENT-SIDE FUNCTIONS (for nostr clients)
// =============================================================================
/**
* Create NIP-42 authentication event (kind 22242)
* @param challenge Challenge string received from relay
* @param relay_url Relay URL (normalized)
* @param private_key 32-byte private key for signing
* @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,
const char* relay_url,
const unsigned char* private_key,
time_t timestamp);
/**
* Create AUTH message JSON for relay communication
* @param auth_event Authentication event (kind 22242)
* @return JSON string for AUTH message or NULL on error (caller must free)
*/
char* nostr_nip42_create_auth_message(cJSON* auth_event);
/**
* Validate challenge string format and freshness
* @param challenge Challenge string to validate
* @param received_at Time when challenge was received (0 for no time check)
* @param time_tolerance Maximum age in seconds (0 for default)
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_validate_challenge(const char* challenge,
time_t received_at,
int time_tolerance);
/**
* Parse AUTH challenge message from relay
* @param message Raw message from relay
* @param challenge_out Output buffer for challenge string
* @param challenge_size Size of challenge buffer
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_parse_auth_challenge(const char* message,
char* challenge_out,
size_t challenge_size);
// =============================================================================
// SERVER-SIDE FUNCTIONS (for relay implementations)
// =============================================================================
/**
* Generate cryptographically secure challenge string
* @param challenge_out Output buffer for challenge (must be at least length*2+1)
* @param length Desired challenge length in bytes (16-128)
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_generate_challenge(char* challenge_out, size_t length);
/**
* Verify NIP-42 authentication event
* @param auth_event Authentication event to verify
* @param expected_challenge Challenge that was sent to client
* @param relay_url Expected relay URL
* @param time_tolerance Maximum timestamp deviation in seconds
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_verify_auth_event(cJSON* auth_event,
const char* expected_challenge,
const char* relay_url,
int time_tolerance);
/**
* Parse AUTH message from client
* @param message Raw AUTH message from client
* @param auth_event_out Output pointer to parsed event (caller must free)
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out);
/**
* Create "auth-required" error response
* @param subscription_id Subscription ID (for CLOSED) or NULL (for OK)
* @param event_id Event ID (for OK) or NULL (for CLOSED)
* @param reason Human-readable reason
* @return JSON string for response or NULL on error (caller must free)
*/
char* nostr_nip42_create_auth_required_message(const char* subscription_id,
const char* event_id,
const char* reason);
/**
* Create "restricted" error response
* @param subscription_id Subscription ID (for CLOSED) or NULL (for OK)
* @param event_id Event ID (for OK) or NULL (for CLOSED)
* @param reason Human-readable reason
* @return JSON string for response or NULL on error (caller must free)
*/
char* nostr_nip42_create_restricted_message(const char* subscription_id,
const char* event_id,
const char* reason);
// =============================================================================
// URL NORMALIZATION FUNCTIONS
// =============================================================================
/**
* Normalize relay URL for comparison (removes trailing slashes, etc.)
* @param url Original URL
* @return Normalized URL string or NULL on error (caller must free)
*/
char* nostr_nip42_normalize_url(const char* url);
/**
* Check if two relay URLs match after normalization
* @param url1 First URL
* @param url2 Second URL
* @return 1 if URLs match, 0 if they don't, -1 on error
*/
int nostr_nip42_urls_match(const char* url1, const char* url2);
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Get string description of authentication state
* @param state Authentication state
* @return Human-readable string
*/
const char* nostr_nip42_auth_state_str(nostr_auth_state_t state);
/**
* Initialize authentication context structure
* @param ctx Context to initialize
* @param relay_url Relay URL
* @param challenge Challenge string
* @param time_tolerance Time tolerance in seconds
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx,
const char* relay_url,
const char* challenge,
int time_tolerance);
/**
* Free authentication context structure
* @param ctx Context to free
*/
void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx);
/**
* Validate authentication event structure (without signature verification)
* @param auth_event Event to validate
* @param relay_url Expected relay URL
* @param challenge Expected challenge
* @param time_tolerance Maximum timestamp deviation in seconds
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_validate_auth_event_structure(cJSON* auth_event,
const char* relay_url,
const char* challenge,
int time_tolerance);
// =============================================================================
// WEBSOCKET CLIENT INTEGRATION
// =============================================================================
// Forward declaration for WebSocket client
struct nostr_ws_client;
/**
* Authenticate WebSocket client with relay
* @param client WebSocket client handle
* @param private_key 32-byte private key for authentication
* @param time_tolerance Maximum timestamp deviation in seconds (0 for default)
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_authenticate(struct nostr_ws_client* client,
const unsigned char* private_key,
int time_tolerance);
/**
* Get current authentication state of WebSocket client
* @param client WebSocket client handle
* @return Current authentication state
*/
nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client);
/**
* Check if WebSocket client has stored valid challenge
* @param client WebSocket client handle
* @return 1 if valid challenge exists, 0 otherwise
*/
int nostr_ws_has_valid_challenge(struct nostr_ws_client* client);
/**
* Get stored challenge from WebSocket client
* @param client WebSocket client handle
* @param challenge_out Output buffer for challenge
* @param challenge_size Size of output buffer
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_get_challenge(struct nostr_ws_client* client,
char* challenge_out,
size_t challenge_size);
/**
* Store challenge in WebSocket client (internal function)
* @param client WebSocket client handle
* @param challenge Challenge string to store
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_store_challenge(struct nostr_ws_client* client,
const char* challenge);
/**
* Clear authentication state in WebSocket client
* @param client WebSocket client handle
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_clear_auth_state(struct nostr_ws_client* client);
#ifdef __cplusplus
}
#endif
#endif // NIP042_H
+511
View File
@@ -0,0 +1,511 @@
/*
* NIP-44: Encrypted Payloads (Versioned) Implementation
* https://github.com/nostr-protocol/nips/blob/master/44.md
*/
#include "nip044.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
int ecdh_shared_secret(const unsigned char* private_key, const unsigned char* public_key, unsigned char* shared_secret);
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// ChaCha20 functions for NIP-44 encryption
int chacha20_encrypt(const unsigned char key[32], unsigned int counter,
const unsigned char nonce[12], const unsigned char* input,
unsigned char* output, size_t length);
// HKDF functions for NIP-44 key derivation
int nostr_hkdf_extract(const unsigned char* salt, size_t salt_len,
const unsigned char* ikm, size_t ikm_len,
unsigned char* prk);
int nostr_hkdf_expand(const unsigned char* prk, size_t prk_len,
const unsigned char* info, size_t info_len,
unsigned char* okm, size_t okm_len);
// HMAC-SHA256 function for NIP-44 authentication
int nostr_hmac_sha256(const unsigned char* key, size_t key_len,
const unsigned char* data, size_t data_len,
unsigned char* hmac);
// Forward declarations for internal functions
static size_t calc_padded_len(size_t unpadded_len);
static unsigned char* pad_plaintext(const char* plaintext, size_t* padded_len);
static char* unpad_plaintext(const unsigned char* padded, size_t padded_len);
static int constant_time_compare(const unsigned char* a, const unsigned char* b, size_t len);
// Memory clearing utility
static void memory_clear(const void *p, size_t len) {
if (p && len) {
memset((void *)p, 0, len);
}
}
// =============================================================================
// NIP-44 UTILITY FUNCTIONS
// =============================================================================
// Constant-time comparison (security critical)
static int constant_time_compare(const unsigned char* a, const unsigned char* b, size_t len) {
unsigned char result = 0;
for (size_t i = 0; i < len; i++) {
result |= (a[i] ^ b[i]);
}
return result == 0;
}
// NIP-44 padding calculation (per spec)
static size_t calc_padded_len(size_t unpadded_len) {
if (unpadded_len <= 32) {
return 32;
}
size_t next_power = 1;
while (next_power < unpadded_len) {
next_power <<= 1;
}
size_t chunk = (next_power <= 256) ? 32 : (next_power / 8);
return chunk * ((unpadded_len - 1) / chunk + 1);
}
// NIP-44 padding (per spec)
static unsigned char* pad_plaintext(const char* plaintext, size_t* padded_len) {
size_t unpadded_len = strlen(plaintext);
if (unpadded_len > 65535) {
return NULL;
}
size_t padded_content_len = calc_padded_len(unpadded_len);
*padded_len = padded_content_len + 2; // Add 2 bytes for the length prefix
unsigned char* padded = malloc(*padded_len);
if (!padded) return NULL;
// Write length prefix (big-endian u16)
padded[0] = (unpadded_len >> 8) & 0xFF;
padded[1] = unpadded_len & 0xFF;
// Copy plaintext and add zero-padding
memcpy(padded + 2, plaintext, unpadded_len);
memset(padded + 2 + unpadded_len, 0, padded_content_len - unpadded_len);
return padded;
}
// NIP-44 unpadding (per spec)
static char* unpad_plaintext(const unsigned char* padded, size_t padded_len) {
if (padded_len < 4) return NULL;
size_t unpadded_len = (padded[0] << 8) | padded[1];
size_t expected_padded_len = calc_padded_len(unpadded_len);
if (padded_len != expected_padded_len + 2) {
return NULL;
}
if (unpadded_len > padded_len - 2) {
return NULL;
}
char* plaintext = malloc(unpadded_len + 1);
if (!plaintext) return NULL;
// Handle empty message case (unpadded_len can be 0)
if (unpadded_len > 0) {
memcpy(plaintext, padded + 2, unpadded_len);
}
plaintext[unpadded_len] = '\0';
return plaintext;
}
// =============================================================================
// NIP-44 IMPLEMENTATION
// =============================================================================
int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
const unsigned char* nonce,
char* output,
size_t output_size) {
if (!sender_private_key || !recipient_public_key || !plaintext || !nonce || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
size_t plaintext_len = strlen(plaintext);
if (plaintext_len > NOSTR_NIP44_MAX_PLAINTEXT_SIZE) {
return NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL;
}
// Step 1: Compute ECDH shared secret
unsigned char shared_secret[32];
if (ecdh_shared_secret(sender_private_key, recipient_public_key, shared_secret) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 2: Calculate conversation key (HKDF-extract with "nip44-v2" as salt)
unsigned char conversation_key[32];
const char* salt_str = "nip44-v2";
if (nostr_hkdf_extract((const unsigned char*)salt_str, strlen(salt_str),
shared_secret, 32, conversation_key) != 0) {
memory_clear(shared_secret, 32);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 3: Use provided nonce (for testing)
// Copy nonce for consistency with existing code structure
unsigned char nonce_copy[32];
memcpy(nonce_copy, nonce, 32);
// Step 4: Derive message keys (HKDF-expand with nonce as info)
unsigned char message_keys[76]; // 32 chacha_key + 12 chacha_nonce + 32 hmac_key
if (nostr_hkdf_expand(conversation_key, 32, nonce_copy, 32, message_keys, 76) != 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce_copy, 32);
return NOSTR_ERROR_CRYPTO_FAILED;
}
unsigned char* chacha_key = message_keys;
unsigned char* chacha_nonce = message_keys + 32;
unsigned char* hmac_key = message_keys + 44;
// Step 5: Pad plaintext according to NIP-44 spec
size_t padded_len;
unsigned char* padded_plaintext = pad_plaintext(plaintext, &padded_len);
if (!padded_plaintext) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 6: Encrypt using ChaCha20
unsigned char* ciphertext = malloc(padded_len);
if (!ciphertext) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
free(padded_plaintext);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (chacha20_encrypt(chacha_key, 0, chacha_nonce, padded_plaintext, ciphertext, padded_len) != 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
free(padded_plaintext);
free(ciphertext);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 7: Compute HMAC with AAD (nonce + ciphertext)
unsigned char* aad_data = malloc(32 + padded_len);
if (!aad_data) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce_copy, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
free(padded_plaintext);
free(ciphertext);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(aad_data, nonce_copy, 32);
memcpy(aad_data + 32, ciphertext, padded_len);
unsigned char mac[32];
if (nostr_hmac_sha256(hmac_key, 32, aad_data, 32 + padded_len, mac) != 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
memory_clear(aad_data, 32 + padded_len);
free(padded_plaintext);
free(ciphertext);
free(aad_data);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 8: Format as base64(version + nonce + ciphertext + mac)
size_t payload_len = 1 + 32 + padded_len + 32; // version + nonce + ciphertext + mac
unsigned char* payload = malloc(payload_len);
if (!payload) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
memory_clear(aad_data, 32 + padded_len);
free(padded_plaintext);
free(ciphertext);
free(aad_data);
return NOSTR_ERROR_MEMORY_FAILED;
}
payload[0] = 0x02; // NIP-44 version 2
memcpy(payload + 1, nonce_copy, 32);
memcpy(payload + 33, ciphertext, padded_len);
memcpy(payload + 33 + padded_len, mac, 32);
// Base64 encode
size_t b64_len = ((payload_len + 2) / 3) * 4 + 1;
if (b64_len > output_size) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
memory_clear(aad_data, 32 + padded_len);
memory_clear(payload, payload_len);
free(padded_plaintext);
free(ciphertext);
free(aad_data);
free(payload);
return NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL;
}
if (base64_encode(payload, payload_len, output, output_size) == 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
memory_clear(aad_data, 32 + padded_len);
memory_clear(payload, payload_len);
free(padded_plaintext);
free(ciphertext);
free(aad_data);
free(payload);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Cleanup
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(nonce_copy, 32);
memory_clear(message_keys, 76);
memory_clear(padded_plaintext, padded_len);
memory_clear(aad_data, 32 + padded_len);
memory_clear(payload, payload_len);
free(padded_plaintext);
free(ciphertext);
free(aad_data);
free(payload);
return NOSTR_SUCCESS;
}
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size) {
// Generate random nonce and call the _with_nonce version
unsigned char nonce[32];
if (nostr_secp256k1_get_random_bytes(nonce, 32) != 1) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return nostr_nip44_encrypt_with_nonce(sender_private_key, recipient_public_key,
plaintext, nonce, output, output_size);
}
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size) {
if (!recipient_private_key || !sender_public_key || !encrypted_data || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Step 1: Base64 decode the encrypted data
size_t max_payload_len = ((strlen(encrypted_data) + 3) / 4) * 3;
unsigned char* payload = malloc(max_payload_len);
if (!payload) {
return NOSTR_ERROR_MEMORY_FAILED;
}
size_t payload_len = base64_decode(encrypted_data, payload);
if (payload_len < 66) { // Minimum: version(1) + nonce(32) + mac(32) + 1 byte ciphertext
free(payload);
return NOSTR_ERROR_NIP44_INVALID_FORMAT;
}
// Step 2: Extract components (version + nonce + ciphertext + mac)
if (payload[0] != 0x02) { // Check NIP-44 version
free(payload);
return NOSTR_ERROR_NIP44_INVALID_FORMAT;
}
unsigned char* nonce = payload + 1;
unsigned char* ciphertext = payload + 33;
unsigned char* received_mac = payload + payload_len - 32;
size_t ciphertext_len = (payload + payload_len - 32) - (payload + 33); // mac_start - ciphertext_start
// Step 3: Compute ECDH shared secret
unsigned char shared_secret[32];
if (ecdh_shared_secret(recipient_private_key, sender_public_key, shared_secret) != 0) {
memory_clear(payload, payload_len);
free(payload);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 4: Calculate conversation key (HKDF-extract with "nip44-v2" as salt)
unsigned char conversation_key[32];
const char* salt_str = "nip44-v2";
if (nostr_hkdf_extract((const unsigned char*)salt_str, strlen(salt_str),
shared_secret, 32, conversation_key) != 0) {
memory_clear(shared_secret, 32);
memory_clear(payload, payload_len);
free(payload);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 5: Derive message keys (HKDF-expand with nonce as info)
unsigned char message_keys[76]; // 32 chacha_key + 12 chacha_nonce + 32 hmac_key
if (nostr_hkdf_expand(conversation_key, 32, nonce, 32, message_keys, 76) != 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(payload, payload_len);
free(payload);
return NOSTR_ERROR_CRYPTO_FAILED;
}
unsigned char* chacha_key = message_keys;
unsigned char* chacha_nonce = message_keys + 32;
unsigned char* hmac_key = message_keys + 44;
// Step 6: Verify HMAC with AAD (nonce + ciphertext)
unsigned char* aad_data = malloc(32 + ciphertext_len);
if (!aad_data) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(payload, payload_len);
free(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(aad_data, nonce, 32);
memcpy(aad_data + 32, ciphertext, ciphertext_len);
unsigned char computed_mac[32];
if (nostr_hmac_sha256(hmac_key, 32, aad_data, 32 + ciphertext_len, computed_mac) != 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
free(aad_data);
free(payload);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Constant-time MAC verification
// Constant-time MAC verification
if (!constant_time_compare(received_mac, computed_mac, 32)) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
free(aad_data);
free(payload);
return NOSTR_ERROR_NIP44_DECRYPT_FAILED;
}
// Step 7: Decrypt using ChaCha20
unsigned char* padded_plaintext = malloc(ciphertext_len);
if (!padded_plaintext) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
free(aad_data);
free(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (chacha20_encrypt(chacha_key, 0, chacha_nonce, ciphertext, padded_plaintext, ciphertext_len) != 0) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
free(aad_data);
free(payload);
free(padded_plaintext);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Step 8: Remove padding according to NIP-44 spec
// Step 8: Remove padding according to NIP-44 spec
char* plaintext = unpad_plaintext(padded_plaintext, ciphertext_len);
if (!plaintext) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
memory_clear(padded_plaintext, ciphertext_len);
free(aad_data);
free(payload);
free(padded_plaintext);
return NOSTR_ERROR_NIP44_DECRYPT_FAILED;
}
// Step 9: Copy to output buffer
size_t plaintext_len = strlen(plaintext);
if (plaintext_len + 1 > output_size) {
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
memory_clear(padded_plaintext, ciphertext_len);
memory_clear(plaintext, plaintext_len);
free(aad_data);
free(payload);
free(padded_plaintext);
free(plaintext);
return NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL;
}
strcpy(output, plaintext);
// Cleanup
memory_clear(shared_secret, 32);
memory_clear(conversation_key, 32);
memory_clear(message_keys, 76);
memory_clear(aad_data, 32 + ciphertext_len);
memory_clear(payload, payload_len);
memory_clear(padded_plaintext, ciphertext_len);
memory_clear(plaintext, plaintext_len);
free(aad_data);
free(payload);
free(padded_plaintext);
free(plaintext);
return NOSTR_SUCCESS;
}
+72
View File
@@ -0,0 +1,72 @@
/*
* NIP-44: Encrypted Payloads (Versioned)
* https://github.com/nostr-protocol/nips/blob/master/44.md
*/
#ifndef NOSTR_NIP044_H
#define NOSTR_NIP044_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// NIP-44 constants
// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 1048576
/**
* NIP-44: Encrypt a message using ECDH + ChaCha20 + HMAC
*
* @param sender_private_key 32-byte sender private key
* @param recipient_public_key 32-byte recipient public key (x-only)
* @param plaintext Message to encrypt
* @param output Buffer for encrypted output (base64 encoded)
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
/**
* NIP-44: Encrypt a message with a specific nonce (for testing)
*
* @param sender_private_key 32-byte sender private key
* @param recipient_public_key 32-byte recipient public key (x-only)
* @param plaintext Message to encrypt
* @param nonce 32-byte nonce
* @param output Buffer for encrypted output (base64 encoded)
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
const unsigned char* nonce,
char* output,
size_t output_size);
/**
* NIP-44: Decrypt a message using ECDH + ChaCha20 + HMAC
*
* @param recipient_private_key 32-byte recipient private key
* @param sender_public_key 32-byte sender public key (x-only)
* @param encrypted_data Encrypted message (base64 encoded)
* @param output Buffer for decrypted plaintext
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP044_H
+987
View File
@@ -0,0 +1,987 @@
/*
* NIP-46: Nostr Remote Signing Implementation
* https://github.com/nostr-protocol/nips/blob/master/46.md
*/
#define _GNU_SOURCE
#include "nip046.h"
#include "nip044.h"
#include "nip004.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
// Forward declarations for crypto/private APIs
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
static char* nip46_strdup(const char* s) {
if (!s) return NULL;
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, s, len + 1);
return out;
}
static void safe_copy(char* dst, size_t dst_size, const char* src) {
if (!dst || dst_size == 0) return;
if (!src) {
dst[0] = '\0';
return;
}
size_t src_len = strlen(src);
size_t copy_len = (src_len < (dst_size - 1U)) ? src_len : (dst_size - 1U);
memcpy(dst, src, copy_len);
dst[copy_len] = '\0';
}
static int is_hex_64(const char* s) {
if (!s || strlen(s) != 64) return 0;
for (int i = 0; i < 64; i++) {
if (!isxdigit((unsigned char)s[i])) return 0;
}
return 1;
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static int url_decode(const char* in, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
size_t oi = 0;
for (size_t i = 0; in[i] != '\0'; i++) {
if (oi + 1 >= out_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
if (in[i] == '%' && in[i + 1] && in[i + 2]) {
int hi = hex_val(in[i + 1]);
int lo = hex_val(in[i + 2]);
if (hi < 0 || lo < 0) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
out[oi++] = (char)((hi << 4) | lo);
i += 2;
} else if (in[i] == '+') {
out[oi++] = ' ';
} else {
out[oi++] = in[i];
}
}
out[oi] = '\0';
return NOSTR_SUCCESS;
}
static int is_unreserved(char c) {
return (isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.' || c == '~');
}
static int url_encode(const char* in, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
static const char* HEX = "0123456789ABCDEF";
size_t oi = 0;
for (size_t i = 0; in[i] != '\0'; i++) {
unsigned char c = (unsigned char)in[i];
if (is_unreserved((char)c)) {
if (oi + 1 >= out_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
out[oi++] = (char)c;
} else {
if (oi + 3 >= out_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
out[oi++] = '%';
out[oi++] = HEX[(c >> 4) & 0x0F];
out[oi++] = HEX[c & 0x0F];
}
}
out[oi] = '\0';
return NOSTR_SUCCESS;
}
const char* nostr_nip46_method_to_string(nostr_nip46_method_t method) {
switch (method) {
case NOSTR_NIP46_METHOD_CONNECT: return "connect";
case NOSTR_NIP46_METHOD_SIGN_EVENT: return "sign_event";
case NOSTR_NIP46_METHOD_PING: return "ping";
case NOSTR_NIP46_METHOD_GET_PUBLIC_KEY: return "get_public_key";
case NOSTR_NIP46_METHOD_NIP04_ENCRYPT: return "nip04_encrypt";
case NOSTR_NIP46_METHOD_NIP04_DECRYPT: return "nip04_decrypt";
case NOSTR_NIP46_METHOD_NIP44_ENCRYPT: return "nip44_encrypt";
case NOSTR_NIP46_METHOD_NIP44_DECRYPT: return "nip44_decrypt";
default: return "unknown";
}
}
nostr_nip46_method_t nostr_nip46_string_to_method(const char* method) {
if (!method) return NOSTR_NIP46_METHOD_UNKNOWN;
if (strcmp(method, "connect") == 0) return NOSTR_NIP46_METHOD_CONNECT;
if (strcmp(method, "sign_event") == 0) return NOSTR_NIP46_METHOD_SIGN_EVENT;
if (strcmp(method, "ping") == 0) return NOSTR_NIP46_METHOD_PING;
if (strcmp(method, "get_public_key") == 0) return NOSTR_NIP46_METHOD_GET_PUBLIC_KEY;
if (strcmp(method, "nip04_encrypt") == 0) return NOSTR_NIP46_METHOD_NIP04_ENCRYPT;
if (strcmp(method, "nip04_decrypt") == 0) return NOSTR_NIP46_METHOD_NIP04_DECRYPT;
if (strcmp(method, "nip44_encrypt") == 0) return NOSTR_NIP46_METHOD_NIP44_ENCRYPT;
if (strcmp(method, "nip44_decrypt") == 0) return NOSTR_NIP46_METHOD_NIP44_DECRYPT;
return NOSTR_NIP46_METHOD_UNKNOWN;
}
int nostr_nip46_generate_request_id(char* output, size_t output_size) {
if (!output || output_size < NOSTR_NIP46_MAX_REQUEST_ID_LEN) {
return NOSTR_ERROR_INVALID_INPUT;
}
unsigned char rnd[16];
if (nostr_secp256k1_get_random_bytes(rnd, sizeof(rnd)) != 1) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
nostr_bytes_to_hex(rnd, sizeof(rnd), output);
return NOSTR_SUCCESS;
}
int nostr_nip46_create_request(const char* id,
nostr_nip46_method_t method,
const char** params,
int param_count,
nostr_nip46_request_t* out) {
if (!id || !out || param_count < 0) return NOSTR_ERROR_INVALID_INPUT;
if (strlen(id) >= sizeof(out->id)) return NOSTR_ERROR_NIP46_INVALID_REQUEST;
memset(out, 0, sizeof(*out));
safe_copy(out->id, sizeof(out->id), id);
out->method = method;
safe_copy(out->method_str, sizeof(out->method_str), nostr_nip46_method_to_string(method));
out->param_count = param_count;
if (param_count == 0) {
out->params = NULL;
return NOSTR_SUCCESS;
}
out->params = (char**)calloc((size_t)param_count, sizeof(char*));
if (!out->params) return NOSTR_ERROR_MEMORY_FAILED;
for (int i = 0; i < param_count; i++) {
const char* p = (params && params[i]) ? params[i] : "";
out->params[i] = nip46_strdup(p);
if (!out->params[i]) {
for (int j = 0; j < i; j++) free(out->params[j]);
free(out->params);
out->params = NULL;
out->param_count = 0;
return NOSTR_ERROR_MEMORY_FAILED;
}
}
return NOSTR_SUCCESS;
}
int nostr_nip46_create_response(const char* id,
const char* result,
const char* error,
nostr_nip46_response_t* out) {
if (!id || !out) return NOSTR_ERROR_INVALID_INPUT;
if (strlen(id) >= sizeof(out->id)) return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
memset(out, 0, sizeof(*out));
safe_copy(out->id, sizeof(out->id), id);
if (result) {
out->result = nip46_strdup(result);
if (!out->result) return NOSTR_ERROR_MEMORY_FAILED;
}
if (error) {
out->error = nip46_strdup(error);
if (!out->error) {
free(out->result);
out->result = NULL;
return NOSTR_ERROR_MEMORY_FAILED;
}
}
return NOSTR_SUCCESS;
}
void nostr_nip46_free_request(nostr_nip46_request_t* request) {
if (!request) return;
if (request->params) {
for (int i = 0; i < request->param_count; i++) {
free(request->params[i]);
}
free(request->params);
}
memset(request, 0, sizeof(*request));
}
void nostr_nip46_free_response(nostr_nip46_response_t* response) {
if (!response) return;
free(response->result);
free(response->error);
memset(response, 0, sizeof(*response));
}
int nostr_nip46_request_to_json(const nostr_nip46_request_t* request, char** output_json) {
if (!request || !output_json) return NOSTR_ERROR_INVALID_INPUT;
cJSON* root = cJSON_CreateObject();
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
cJSON_AddStringToObject(root, "id", request->id);
cJSON_AddStringToObject(root, "method", request->method_str);
cJSON* params = cJSON_CreateArray();
if (!params) {
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < request->param_count; i++) {
cJSON_AddItemToArray(params, cJSON_CreateString(request->params[i] ? request->params[i] : ""));
}
cJSON_AddItemToObject(root, "params", params);
char* js = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!js) return NOSTR_ERROR_MEMORY_FAILED;
*output_json = js;
return NOSTR_SUCCESS;
}
int nostr_nip46_response_to_json(const nostr_nip46_response_t* response, char** output_json) {
if (!response || !output_json) return NOSTR_ERROR_INVALID_INPUT;
cJSON* root = cJSON_CreateObject();
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
cJSON_AddStringToObject(root, "id", response->id);
cJSON_AddStringToObject(root, "result", response->result ? response->result : "");
if (response->error) {
cJSON_AddStringToObject(root, "error", response->error);
}
char* js = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!js) return NOSTR_ERROR_MEMORY_FAILED;
*output_json = js;
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_request(const char* json_payload, nostr_nip46_request_t* out) {
if (!json_payload || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
cJSON* root = cJSON_Parse(json_payload);
if (!root) return NOSTR_ERROR_NIP46_INVALID_REQUEST;
cJSON* id = cJSON_GetObjectItem(root, "id");
cJSON* method = cJSON_GetObjectItem(root, "method");
cJSON* params = cJSON_GetObjectItem(root, "params");
if (!cJSON_IsString(id) || !cJSON_IsString(method) || !cJSON_IsArray(params)) {
cJSON_Delete(root);
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
safe_copy(out->id, sizeof(out->id), cJSON_GetStringValue(id));
safe_copy(out->method_str, sizeof(out->method_str), cJSON_GetStringValue(method));
out->method = nostr_nip46_string_to_method(out->method_str);
out->param_count = cJSON_GetArraySize(params);
if (out->param_count > 0) {
out->params = (char**)calloc((size_t)out->param_count, sizeof(char*));
if (!out->params) {
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < out->param_count; i++) {
cJSON* p = cJSON_GetArrayItem(params, i);
if (!cJSON_IsString(p)) {
nostr_nip46_free_request(out);
cJSON_Delete(root);
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
out->params[i] = nip46_strdup(cJSON_GetStringValue(p));
if (!out->params[i]) {
nostr_nip46_free_request(out);
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
}
cJSON_Delete(root);
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_response(const char* json_payload, nostr_nip46_response_t* out) {
if (!json_payload || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
cJSON* root = cJSON_Parse(json_payload);
if (!root) return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
cJSON* id = cJSON_GetObjectItem(root, "id");
cJSON* result = cJSON_GetObjectItem(root, "result");
cJSON* error = cJSON_GetObjectItem(root, "error");
if (!cJSON_IsString(id) || !cJSON_IsString(result)) {
cJSON_Delete(root);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
safe_copy(out->id, sizeof(out->id), cJSON_GetStringValue(id));
out->result = nip46_strdup(cJSON_GetStringValue(result));
if (!out->result) {
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (error && cJSON_IsString(error)) {
out->error = nip46_strdup(cJSON_GetStringValue(error));
if (!out->error) {
nostr_nip46_free_response(out);
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
cJSON_Delete(root);
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;
char recipient_hex[65];
nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex);
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* ptag = cJSON_CreateArray();
if (!ptag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(ptag, cJSON_CreateString("p"));
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_Delete(tags);
return evt;
}
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;
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;
return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted,
sender_private_key, recipient_public_key, 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) {
if (!response || !sender_private_key || !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;
return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted,
sender_private_key, recipient_public_key, timestamp);
}
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) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* kind = cJSON_GetObjectItem(event, "kind");
if (!cJSON_IsString(content) || !cJSON_IsString(pubkey) || !cJSON_IsNumber(kind)) {
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
if ((int)cJSON_GetNumberValue(kind) != NOSTR_NIP46_EVENT_KIND) {
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);
if (rc != NOSTR_SUCCESS) return NOSTR_ERROR_NIP46_DECRYPTION_FAILED;
return NOSTR_SUCCESS;
}
static int parse_query_pairs(const char* query,
int (*cb)(const char* key, const char* val, void* ctx),
void* ctx) {
if (!query || !cb) return NOSTR_ERROR_INVALID_INPUT;
char* work = nip46_strdup(query);
if (!work) return NOSTR_ERROR_MEMORY_FAILED;
char* saveptr = NULL;
char* token = strtok_r(work, "&", &saveptr);
while (token) {
char* eq = strchr(token, '=');
if (eq) {
*eq = '\0';
const char* key = token;
const char* val = eq + 1;
int rc = cb(key, val, ctx);
if (rc != NOSTR_SUCCESS) {
free(work);
return rc;
}
}
token = strtok_r(NULL, "&", &saveptr);
}
free(work);
return NOSTR_SUCCESS;
}
typedef struct {
nostr_nip46_bunker_url_t* out;
} bunker_parse_ctx_t;
static int bunker_pair_cb(const char* key, const char* val, void* ctx) {
bunker_parse_ctx_t* pctx = (bunker_parse_ctx_t*)ctx;
char decoded[512];
int rc = url_decode(val, decoded, sizeof(decoded));
if (rc != NOSTR_SUCCESS) return rc;
if (strcmp(key, "relay") == 0) {
if (pctx->out->relay_count >= NOSTR_NIP46_MAX_RELAYS) return NOSTR_SUCCESS;
safe_copy(pctx->out->relays[pctx->out->relay_count],
sizeof(pctx->out->relays[pctx->out->relay_count]), decoded);
pctx->out->relay_count++;
} else if (strcmp(key, "secret") == 0) {
safe_copy(pctx->out->secret, sizeof(pctx->out->secret), decoded);
}
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_bunker_url(const char* url, nostr_nip46_bunker_url_t* out) {
if (!url || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
const char* prefix = "bunker://";
size_t prefix_len = strlen(prefix);
if (strncmp(url, prefix, prefix_len) != 0) {
return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
}
const char* after = url + prefix_len;
const char* q = strchr(after, '?');
char pubkey[65];
if (!q) {
safe_copy(pubkey, sizeof(pubkey), after);
} else {
size_t l = (size_t)(q - after);
if (l >= sizeof(pubkey)) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
memcpy(pubkey, after, l);
pubkey[l] = '\0';
}
if (!is_hex_64(pubkey)) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
safe_copy(out->remote_signer_pubkey, sizeof(out->remote_signer_pubkey), pubkey);
if (q && *(q + 1)) {
bunker_parse_ctx_t ctx = { out };
int rc = parse_query_pairs(q + 1, bunker_pair_cb, &ctx);
if (rc != NOSTR_SUCCESS) return rc;
}
return NOSTR_SUCCESS;
}
typedef struct {
nostr_nip46_nostrconnect_url_t* out;
} nostrconnect_parse_ctx_t;
static int nostrconnect_pair_cb(const char* key, const char* val, void* ctx) {
nostrconnect_parse_ctx_t* pctx = (nostrconnect_parse_ctx_t*)ctx;
char decoded[1024];
int rc = url_decode(val, decoded, sizeof(decoded));
if (rc != NOSTR_SUCCESS) return rc;
if (strcmp(key, "relay") == 0) {
if (pctx->out->relay_count >= NOSTR_NIP46_MAX_RELAYS) return NOSTR_SUCCESS;
safe_copy(pctx->out->relays[pctx->out->relay_count],
sizeof(pctx->out->relays[pctx->out->relay_count]), decoded);
pctx->out->relay_count++;
} else if (strcmp(key, "secret") == 0) {
safe_copy(pctx->out->secret, sizeof(pctx->out->secret), decoded);
} else if (strcmp(key, "perms") == 0) {
safe_copy(pctx->out->perms, sizeof(pctx->out->perms), decoded);
} else if (strcmp(key, "name") == 0) {
safe_copy(pctx->out->name, sizeof(pctx->out->name), decoded);
} else if (strcmp(key, "url") == 0) {
safe_copy(pctx->out->url, sizeof(pctx->out->url), decoded);
} else if (strcmp(key, "image") == 0) {
safe_copy(pctx->out->image, sizeof(pctx->out->image), decoded);
}
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_nostrconnect_url(const char* url, nostr_nip46_nostrconnect_url_t* out) {
if (!url || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
const char* prefix = "nostrconnect://";
size_t prefix_len = strlen(prefix);
if (strncmp(url, prefix, prefix_len) != 0) {
return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
}
const char* after = url + prefix_len;
const char* q = strchr(after, '?');
char pubkey[65];
if (!q) {
safe_copy(pubkey, sizeof(pubkey), after);
} else {
size_t l = (size_t)(q - after);
if (l >= sizeof(pubkey)) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
memcpy(pubkey, after, l);
pubkey[l] = '\0';
}
if (!is_hex_64(pubkey)) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
safe_copy(out->client_pubkey, sizeof(out->client_pubkey), pubkey);
if (q && *(q + 1)) {
nostrconnect_parse_ctx_t ctx = { out };
int rc = parse_query_pairs(q + 1, nostrconnect_pair_cb, &ctx);
if (rc != NOSTR_SUCCESS) return rc;
}
if (out->relay_count <= 0 || out->secret[0] == '\0') {
return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
}
return NOSTR_SUCCESS;
}
int nostr_nip46_create_bunker_url(const nostr_nip46_bunker_url_t* in, char* output, size_t output_size) {
if (!in || !output || output_size == 0) return NOSTR_ERROR_INVALID_INPUT;
if (!is_hex_64(in->remote_signer_pubkey)) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
size_t used = (size_t)snprintf(output, output_size, "bunker://%s", in->remote_signer_pubkey);
if (used >= output_size) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
int first = 1;
for (int i = 0; i < in->relay_count; i++) {
char enc[768];
int rc = url_encode(in->relays[i], enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%crelay=%s", first ? '?' : '&', enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
used += (size_t)wrote;
first = 0;
}
if (in->secret[0]) {
char encs[512];
int rc = url_encode(in->secret, encs, sizeof(encs));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%csecret=%s", first ? '?' : '&', encs);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
}
return NOSTR_SUCCESS;
}
int nostr_nip46_create_nostrconnect_url(const nostr_nip46_nostrconnect_url_t* in,
char* output,
size_t output_size) {
if (!in || !output || output_size == 0) return NOSTR_ERROR_INVALID_INPUT;
if (!is_hex_64(in->client_pubkey)) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
if (in->relay_count <= 0 || in->secret[0] == '\0') return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
size_t used = (size_t)snprintf(output, output_size, "nostrconnect://%s", in->client_pubkey);
if (used >= output_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
int first = 1;
for (int i = 0; i < in->relay_count; i++) {
char enc[768];
int rc = url_encode(in->relays[i], enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%crelay=%s", first ? '?' : '&', enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
first = 0;
}
char enc_secret[512];
int rc = url_encode(in->secret, enc_secret, sizeof(enc_secret));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%csecret=%s", first ? '?' : '&', enc_secret);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
if (in->perms[0]) {
char enc[1024];
rc = url_encode(in->perms, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&perms=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
}
if (in->name[0]) {
char enc[512];
rc = url_encode(in->name, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&name=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
}
if (in->url[0]) {
char enc[768];
rc = url_encode(in->url, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&url=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
}
if (in->image[0]) {
char enc[768];
rc = url_encode(in->image, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&image=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
}
return NOSTR_SUCCESS;
}
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;
memset(session, 0, sizeof(*session));
memcpy(session->client_private_key, client_private_key, 32);
unsigned char client_pub[32];
if (nostr_ec_public_key_from_private_key(client_private_key, client_pub) != 0) {
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;
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) {
return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
}
session->relay_count = parsed.relay_count;
for (int i = 0; i < parsed.relay_count; i++) {
safe_copy(session->relays[i], sizeof(session->relays[i]), parsed.relays[i]);
}
session->connected = 0;
return NOSTR_SUCCESS;
}
void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session) {
if (!session) return;
memset(session, 0, sizeof(*session));
}
static int client_make_request_event(nostr_nip46_client_session_t* session,
nostr_nip46_method_t method,
const char** params,
int param_count,
cJSON** request_event_out) {
if (!session || !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));
if (rc != NOSTR_SUCCESS) return rc;
nostr_nip46_request_t req;
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);
nostr_nip46_free_request(&req);
if (!evt) return NOSTR_ERROR_NIP46_ENCRYPTION_FAILED;
*request_event_out = evt;
return NOSTR_SUCCESS;
}
int nostr_nip46_client_connect(nostr_nip46_client_session_t* session,
const char* optional_secret,
const char* optional_permissions,
cJSON** request_event_out) {
if (!session || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
const char* params[3];
int count = 1;
params[0] = session->remote_signer_pubkey_hex;
if (optional_secret && optional_secret[0]) {
params[count++] = optional_secret;
}
if (optional_permissions && optional_permissions[0]) {
params[count++] = optional_permissions;
}
return client_make_request_event(session, NOSTR_NIP46_METHOD_CONNECT, params, count, request_event_out);
}
int nostr_nip46_client_get_public_key(nostr_nip46_client_session_t* session,
cJSON** request_event_out) {
return client_make_request_event(session, NOSTR_NIP46_METHOD_GET_PUBLIC_KEY, NULL, 0, request_event_out);
}
int nostr_nip46_client_ping(nostr_nip46_client_session_t* session,
cJSON** request_event_out) {
return client_make_request_event(session, NOSTR_NIP46_METHOD_PING, NULL, 0, request_event_out);
}
int nostr_nip46_client_sign_event(nostr_nip46_client_session_t* session,
cJSON* unsigned_event,
cJSON** request_event_out) {
if (!session || !unsigned_event || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
char* ev = cJSON_PrintUnformatted(unsigned_event);
if (!ev) return NOSTR_ERROR_MEMORY_FAILED;
const char* params[1] = { ev };
int rc = client_make_request_event(session, NOSTR_NIP46_METHOD_SIGN_EVENT, params, 1, request_event_out);
free(ev);
return rc;
}
int nostr_nip46_signer_session_init(nostr_nip46_signer_session_t* session,
const unsigned char* signer_private_key,
const unsigned char* user_private_key,
const char** relays,
int relay_count) {
if (!session || !signer_private_key || !user_private_key) return NOSTR_ERROR_INVALID_INPUT;
memset(session, 0, sizeof(*session));
memcpy(session->signer_private_key, signer_private_key, 32);
memcpy(session->user_private_key, user_private_key, 32);
unsigned char signer_pub[32];
unsigned char user_pub[32];
if (nostr_ec_public_key_from_private_key(signer_private_key, signer_pub) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
if (nostr_ec_public_key_from_private_key(user_private_key, user_pub) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
nostr_bytes_to_hex(signer_pub, 32, session->signer_pubkey_hex);
nostr_bytes_to_hex(user_pub, 32, session->user_pubkey_hex);
if (relay_count > NOSTR_NIP46_MAX_RELAYS) relay_count = NOSTR_NIP46_MAX_RELAYS;
session->relay_count = relay_count;
for (int i = 0; i < relay_count; i++) {
safe_copy(session->relays[i], sizeof(session->relays[i]), relays[i]);
}
session->connected = 0;
return NOSTR_SUCCESS;
}
void nostr_nip46_signer_session_destroy(nostr_nip46_signer_session_t* session) {
if (!session) return;
memset(session, 0, sizeof(*session));
}
int nostr_nip46_signer_create_bunker_url(const nostr_nip46_signer_session_t* session,
const char* optional_secret,
char* output,
size_t output_size) {
if (!session || !output) return NOSTR_ERROR_INVALID_INPUT;
nostr_nip46_bunker_url_t b;
memset(&b, 0, sizeof(b));
safe_copy(b.remote_signer_pubkey, sizeof(b.remote_signer_pubkey), session->signer_pubkey_hex);
b.relay_count = session->relay_count;
for (int i = 0; i < session->relay_count; i++) {
safe_copy(b.relays[i], sizeof(b.relays[i]), session->relays[i]);
}
if (optional_secret) {
safe_copy(b.secret, sizeof(b.secret), optional_secret);
}
return nostr_nip46_create_bunker_url(&b, output, output_size);
}
static int signer_make_ok(const char* id, const char* result, nostr_nip46_response_t* out) {
return nostr_nip46_create_response(id, result, NULL, out);
}
static int signer_make_err(const char* id, const char* err, nostr_nip46_response_t* out) {
return nostr_nip46_create_response(id, "", err, out);
}
int nostr_nip46_signer_handle_request(nostr_nip46_signer_session_t* session,
const nostr_nip46_request_t* request,
nostr_nip46_response_t* response_out) {
if (!session || !request || !response_out) return NOSTR_ERROR_INVALID_INPUT;
switch (request->method) {
case NOSTR_NIP46_METHOD_CONNECT: {
if (request->param_count >= 1 && request->params[0] && request->params[0][0]) {
if (strcmp(request->params[0], session->signer_pubkey_hex) != 0) {
return signer_make_err(request->id, "remote signer pubkey mismatch", response_out);
}
}
session->connected = 1;
if (request->param_count >= 2 && request->params[1] && request->params[1][0]) {
return signer_make_ok(request->id, request->params[1], response_out);
}
return signer_make_ok(request->id, "ack", response_out);
}
case NOSTR_NIP46_METHOD_PING:
return signer_make_ok(request->id, "pong", response_out);
case NOSTR_NIP46_METHOD_GET_PUBLIC_KEY:
return signer_make_ok(request->id, session->user_pubkey_hex, response_out);
case NOSTR_NIP46_METHOD_SIGN_EVENT: {
if (request->param_count < 1 || !request->params[0]) {
return signer_make_err(request->id, "missing sign_event payload", response_out);
}
cJSON* in = cJSON_Parse(request->params[0]);
if (!in) return signer_make_err(request->id, "invalid sign_event JSON", response_out);
cJSON* kind = cJSON_GetObjectItem(in, "kind");
cJSON* content = cJSON_GetObjectItem(in, "content");
cJSON* tags = cJSON_GetObjectItem(in, "tags");
cJSON* created = cJSON_GetObjectItem(in, "created_at");
if (!cJSON_IsNumber(kind) || !cJSON_IsString(content)) {
cJSON_Delete(in);
return signer_make_err(request->id, "invalid sign_event fields", response_out);
}
time_t ts = 0;
if (created && cJSON_IsNumber(created)) {
ts = (time_t)cJSON_GetNumberValue(created);
}
cJSON* signed_evt = nostr_create_and_sign_event((int)cJSON_GetNumberValue(kind),
cJSON_GetStringValue(content),
cJSON_IsArray(tags) ? tags : NULL,
session->user_private_key,
ts);
cJSON_Delete(in);
if (!signed_evt) {
return signer_make_err(request->id, "failed to sign event", response_out);
}
char* signed_json = cJSON_PrintUnformatted(signed_evt);
cJSON_Delete(signed_evt);
if (!signed_json) {
return signer_make_err(request->id, "failed to encode signed event", response_out);
}
int rc = signer_make_ok(request->id, signed_json, response_out);
free(signed_json);
return rc;
}
case NOSTR_NIP46_METHOD_NIP04_ENCRYPT:
case NOSTR_NIP46_METHOD_NIP04_DECRYPT:
case NOSTR_NIP46_METHOD_NIP44_ENCRYPT:
case NOSTR_NIP46_METHOD_NIP44_DECRYPT: {
if (request->param_count < 2 || !request->params[0] || !request->params[1]) {
return signer_make_err(request->id, "missing crypto params", response_out);
}
unsigned char peer_pub[32];
if (nostr_hex_to_bytes(request->params[0], peer_pub, 32) != 0) {
return signer_make_err(request->id, "invalid peer pubkey", response_out);
}
char out_buf[NOSTR_NIP46_MAX_PAYLOAD_LEN];
int rc;
if (request->method == NOSTR_NIP46_METHOD_NIP04_ENCRYPT) {
rc = nostr_nip04_encrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
} else if (request->method == NOSTR_NIP46_METHOD_NIP04_DECRYPT) {
rc = nostr_nip04_decrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
} else if (request->method == NOSTR_NIP46_METHOD_NIP44_ENCRYPT) {
rc = nostr_nip44_encrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
} else {
rc = nostr_nip44_decrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
}
if (rc != NOSTR_SUCCESS) {
return signer_make_err(request->id, nostr_strerror(rc), response_out);
}
return signer_make_ok(request->id, out_buf, response_out);
}
default:
return signer_make_err(request->id, "unknown method", response_out);
}
}
+181
View File
@@ -0,0 +1,181 @@
/*
* NIP-46: Nostr Remote Signing
* https://github.com/nostr-protocol/nips/blob/master/46.md
*/
#ifndef NOSTR_NIP046_H
#define NOSTR_NIP046_H
#include <stddef.h>
#include <time.h>
#include "nostr_common.h"
#include "nip001.h"
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NOSTR_NIP46_EVENT_KIND 24133
#define NOSTR_NIP46_MAX_RELAYS 8
#define NOSTR_NIP46_MAX_RELAY_URL_LEN 256
#define NOSTR_NIP46_MAX_SECRET_LEN 128
#define NOSTR_NIP46_MAX_REQUEST_ID_LEN 65
#define NOSTR_NIP46_MAX_METHOD_LEN 32
#define NOSTR_NIP46_MAX_URL_LEN 2048
#define NOSTR_NIP46_MAX_PAYLOAD_LEN 65536
typedef enum {
NOSTR_NIP46_CONN_BUNKER = 0,
NOSTR_NIP46_CONN_NOSTRCONNECT = 1
} nostr_nip46_connection_type_t;
typedef enum {
NOSTR_NIP46_METHOD_CONNECT = 0,
NOSTR_NIP46_METHOD_SIGN_EVENT,
NOSTR_NIP46_METHOD_PING,
NOSTR_NIP46_METHOD_GET_PUBLIC_KEY,
NOSTR_NIP46_METHOD_NIP04_ENCRYPT,
NOSTR_NIP46_METHOD_NIP04_DECRYPT,
NOSTR_NIP46_METHOD_NIP44_ENCRYPT,
NOSTR_NIP46_METHOD_NIP44_DECRYPT,
NOSTR_NIP46_METHOD_UNKNOWN
} nostr_nip46_method_t;
typedef struct {
char remote_signer_pubkey[65];
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
int relay_count;
char secret[NOSTR_NIP46_MAX_SECRET_LEN];
} nostr_nip46_bunker_url_t;
typedef struct {
char client_pubkey[65];
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
int relay_count;
char secret[NOSTR_NIP46_MAX_SECRET_LEN];
char perms[512];
char name[128];
char url[256];
char image[256];
} nostr_nip46_nostrconnect_url_t;
typedef struct {
char id[NOSTR_NIP46_MAX_REQUEST_ID_LEN];
nostr_nip46_method_t method;
char method_str[NOSTR_NIP46_MAX_METHOD_LEN];
char** params;
int param_count;
} nostr_nip46_request_t;
typedef struct {
char id[NOSTR_NIP46_MAX_REQUEST_ID_LEN];
char* result;
char* error;
} nostr_nip46_response_t;
typedef struct {
unsigned char client_private_key[32];
char client_pubkey_hex[65];
char remote_signer_pubkey_hex[65];
unsigned char remote_signer_pubkey[32];
char user_pubkey_hex[65];
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
int relay_count;
int connected;
} nostr_nip46_client_session_t;
typedef struct {
unsigned char signer_private_key[32];
char signer_pubkey_hex[65];
unsigned char user_private_key[32];
char user_pubkey_hex[65];
char client_pubkey_hex[65];
unsigned char client_pubkey[32];
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
int relay_count;
int connected;
} nostr_nip46_signer_session_t;
/* URL parsing/creation */
int nostr_nip46_parse_bunker_url(const char* url, nostr_nip46_bunker_url_t* out);
int nostr_nip46_parse_nostrconnect_url(const char* url, nostr_nip46_nostrconnect_url_t* out);
int nostr_nip46_create_bunker_url(const nostr_nip46_bunker_url_t* in, char* output, size_t output_size);
int nostr_nip46_create_nostrconnect_url(const nostr_nip46_nostrconnect_url_t* in, char* output, size_t output_size);
/* Method conversion */
const char* nostr_nip46_method_to_string(nostr_nip46_method_t method);
nostr_nip46_method_t nostr_nip46_string_to_method(const char* method);
/* Request/response object utilities */
int nostr_nip46_generate_request_id(char* output, size_t output_size);
int nostr_nip46_create_request(const char* id,
nostr_nip46_method_t method,
const char** params,
int param_count,
nostr_nip46_request_t* out);
int nostr_nip46_create_response(const char* id,
const char* result,
const char* error,
nostr_nip46_response_t* out);
int nostr_nip46_parse_request(const char* json_payload, nostr_nip46_request_t* out);
int nostr_nip46_parse_response(const char* json_payload, nostr_nip46_response_t* out);
void nostr_nip46_free_request(nostr_nip46_request_t* request);
void nostr_nip46_free_response(nostr_nip46_response_t* response);
/* JSON payload serialization */
int nostr_nip46_request_to_json(const nostr_nip46_request_t* request, char** output_json);
int nostr_nip46_response_to_json(const nostr_nip46_response_t* response, char** output_json);
/* Event creation/decryption */
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_response_event(const nostr_nip46_response_t* response,
const unsigned char* sender_private_key,
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);
/* Client session */
int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session,
const unsigned char* client_private_key,
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,
const char* optional_secret,
const char* optional_permissions,
cJSON** request_event_out);
int nostr_nip46_client_get_public_key(nostr_nip46_client_session_t* session,
cJSON** request_event_out);
int nostr_nip46_client_ping(nostr_nip46_client_session_t* session,
cJSON** request_event_out);
int nostr_nip46_client_sign_event(nostr_nip46_client_session_t* session,
cJSON* unsigned_event,
cJSON** request_event_out);
/* Signer session */
int nostr_nip46_signer_session_init(nostr_nip46_signer_session_t* session,
const unsigned char* signer_private_key,
const unsigned char* user_private_key,
const char** relays,
int relay_count);
void nostr_nip46_signer_session_destroy(nostr_nip46_signer_session_t* session);
int nostr_nip46_signer_handle_request(nostr_nip46_signer_session_t* session,
const nostr_nip46_request_t* request,
nostr_nip46_response_t* response_out);
int nostr_nip46_signer_create_bunker_url(const nostr_nip46_signer_session_t* session,
const char* optional_secret,
char* output,
size_t output_size);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_NIP046_H */
+412
View File
@@ -0,0 +1,412 @@
/*
* NIP-59: Gift Wrap Implementation
* https://github.com/nostr-protocol/nips/blob/master/59.md
*/
#include "nip059.h"
#include "nip044.h"
#include "nip001.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature);
// Memory clearing utility
static void memory_clear(const void *p, size_t len) {
if (p && len) {
memset((void *)p, 0, len);
}
}
/**
* Create a random timestamp within max_delay_sec in the past (configurable)
*/
static time_t random_past_timestamp(long max_delay_sec) {
time_t now = time(NULL);
// If max_delay_sec is 0, return current timestamp (no randomization)
if (max_delay_sec == 0) {
return now;
}
// Random time up to max_delay_sec in the past
long random_offset = (long)(rand() % max_delay_sec);
return now - random_offset;
}
/**
* Generate a random private key for gift wrap
*/
static int generate_random_private_key(unsigned char* private_key) {
return nostr_secp256k1_get_random_bytes(private_key, 32);
}
/**
* Create event ID from event data (without signature)
*/
static int create_event_id(cJSON* event, char* event_id_hex) {
if (!event || !event_id_hex) {
return -1;
}
// 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) {
return -1;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
return -1;
}
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) {
return -1;
}
// 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);
return -1;
}
// Convert hash to hex
nostr_bytes_to_hex(event_hash, 32, event_id_hex);
free(serialize_string);
return 0;
}
/**
* NIP-59: Create a rumor (unsigned event)
*/
cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
const char* pubkey_hex, time_t created_at) {
if (!pubkey_hex || !content) {
return NULL;
}
// Use provided timestamp or random past timestamp (default to 0 for compatibility)
time_t event_time = (created_at == 0) ? random_past_timestamp(0) : created_at;
// Create event structure (without id and sig - that's what makes it a rumor)
cJSON* rumor = cJSON_CreateObject();
if (!rumor) {
return NULL;
}
cJSON_AddStringToObject(rumor, "pubkey", pubkey_hex);
cJSON_AddNumberToObject(rumor, "created_at", (double)event_time);
cJSON_AddNumberToObject(rumor, "kind", kind);
// Add tags (copy provided tags or create empty array)
if (tags) {
cJSON_AddItemToObject(rumor, "tags", cJSON_Duplicate(tags, 1));
} else {
cJSON_AddItemToObject(rumor, "tags", cJSON_CreateArray());
}
cJSON_AddStringToObject(rumor, "content", content);
// Calculate and add event ID
char event_id[65];
if (create_event_id(rumor, event_id) != 0) {
cJSON_Delete(rumor);
return NULL;
}
cJSON_AddStringToObject(rumor, "id", event_id);
return rumor;
}
/**
* NIP-59: Create a seal (kind 13) wrapping a rumor
*/
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) {
return NULL;
}
// Serialize the rumor to JSON
char* rumor_json = cJSON_PrintUnformatted(rumor);
if (!rumor_json) {
return NULL;
}
// Encrypt the rumor using NIP-44
char encrypted_content[4096]; // Should be large enough for most events
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
rumor_json, encrypted_content, sizeof(encrypted_content));
free(rumor_json);
if (encrypt_result != NOSTR_SUCCESS) {
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) {
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();
if (!seal) {
return NULL;
}
time_t 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_AddStringToObject(seal, "content", 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);
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;
}
/**
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
*/
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex, long max_delay_sec) {
if (!seal || !recipient_public_key_hex) {
return NULL;
}
// Serialize the seal to JSON
char* seal_json = cJSON_PrintUnformatted(seal);
if (!seal_json) {
return NULL;
}
// Generate random private key for gift wrap
unsigned char random_private_key[32];
if (generate_random_private_key(random_private_key) != 1) {
free(seal_json);
return NULL;
}
// Get random public key
unsigned char random_public_key[32];
if (nostr_ec_public_key_from_private_key(random_private_key, random_public_key) != 0) {
memory_clear(random_private_key, 32);
free(seal_json);
return NULL;
}
char random_pubkey_hex[65];
nostr_bytes_to_hex(random_public_key, 32, random_pubkey_hex);
// Convert recipient pubkey hex to bytes
unsigned char recipient_public_key[32];
if (nostr_hex_to_bytes(recipient_public_key_hex, recipient_public_key, 32) != 0) {
memory_clear(random_private_key, 32);
free(seal_json);
return NULL;
}
// Encrypt the seal using NIP-44
char encrypted_content[8192]; // Larger buffer for nested encryption
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
seal_json, encrypted_content, sizeof(encrypted_content));
free(seal_json);
if (encrypt_result != NOSTR_SUCCESS) {
memory_clear(random_private_key, 32);
return NULL;
}
// Create gift wrap event (kind 1059)
cJSON* gift_wrap = cJSON_CreateObject();
if (!gift_wrap) {
memory_clear(random_private_key, 32);
return NULL;
}
time_t wrap_time = random_past_timestamp(max_delay_sec);
cJSON_AddStringToObject(gift_wrap, "pubkey", random_pubkey_hex);
cJSON_AddNumberToObject(gift_wrap, "created_at", (double)wrap_time);
cJSON_AddNumberToObject(gift_wrap, "kind", 1059);
// Add p tag for recipient
cJSON* tags = cJSON_CreateArray();
cJSON* p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_public_key_hex));
cJSON_AddItemToArray(tags, p_tag);
cJSON_AddItemToObject(gift_wrap, "tags", tags);
cJSON_AddStringToObject(gift_wrap, "content", encrypted_content);
// Calculate event ID
char event_id[65];
if (create_event_id(gift_wrap, event_id) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
cJSON_AddStringToObject(gift_wrap, "id", event_id);
// Sign the gift wrap
unsigned char event_hash[32];
if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
unsigned char signature[64];
if (nostr_ec_sign(random_private_key, event_hash, signature) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
cJSON_AddStringToObject(gift_wrap, "sig", sig_hex);
// Clear the random private key from memory
memory_clear(random_private_key, 32);
return gift_wrap;
}
/**
* 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) {
return NULL;
}
// Get the encrypted content
cJSON* content_item = cJSON_GetObjectItem(gift_wrap, "content");
if (!content_item || !cJSON_IsString(content_item)) {
return NULL;
}
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Get the sender's public key (gift wrap pubkey)
cJSON* pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey");
if (!pubkey_item || !cJSON_IsString(pubkey_item)) {
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) {
return NULL;
}
// Decrypt the content using NIP-44
char decrypted_json[8192];
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
if (decrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Parse the decrypted JSON as the seal event
cJSON* seal = cJSON_Parse(decrypted_json);
if (!seal) {
return NULL;
}
return seal;
}
/**
* NIP-59: Unseal a seal to get the rumor
*/
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) {
return NULL;
}
// Get the encrypted content
cJSON* content_item = cJSON_GetObjectItem(seal, "content");
if (!content_item || !cJSON_IsString(content_item)) {
return NULL;
}
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Decrypt the content using NIP-44
char decrypted_json[4096];
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
if (decrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Parse the decrypted JSON as the rumor event
cJSON* rumor = cJSON_Parse(decrypted_json);
if (!rumor) {
return NULL;
}
return rumor;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* NIP-59: Gift Wrap
* https://github.com/nostr-protocol/nips/blob/master/59.md
*/
#ifndef NOSTR_NIP059_H
#define NOSTR_NIP059_H
#include <stddef.h>
#include <time.h>
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* NIP-59: Create a rumor (unsigned event)
*
* @param kind Event kind
* @param content Event content
* @param tags Event tags (cJSON array, can be NULL)
* @param pubkey_hex Sender's public key in hex format
* @param created_at Event timestamp (0 for current time)
* @return cJSON object representing the rumor, or NULL on error
*/
cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
const char* pubkey_hex, time_t created_at);
/**
* NIP-59: Create a seal (kind 13) wrapping a rumor
*
* @param rumor The rumor event to seal (cJSON object)
* @param sender_private_key 32-byte sender private key
* @param recipient_public_key 32-byte recipient public key (x-only)
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
* @return cJSON object representing the seal event, or NULL on error
*/
cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key,
const unsigned char* recipient_public_key, long max_delay_sec);
/**
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
*
* @param seal The seal event to wrap (cJSON object)
* @param recipient_public_key_hex Recipient's public key in hex format
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
* @return cJSON object representing the gift wrap event, or NULL on error
*/
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex, long max_delay_sec);
/**
* NIP-59: Unwrap a gift wrap to get the seal
*
* @param gift_wrap The gift wrap event (cJSON object)
* @param recipient_private_key 32-byte recipient private key
* @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);
/**
* NIP-59: Unseal a seal to get the rumor
*
* @param seal The seal event (cJSON object)
* @param sender_public_key 32-byte sender public key (x-only)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the rumor event, or NULL on error
*/
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
const unsigned char* recipient_private_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP059_H
+791
View File
@@ -0,0 +1,791 @@
/*
* NIP-60: Cashu Wallet Implementation
* https://github.com/nostr-protocol/nips/blob/master/60.md
*/
#include "nip060.h"
#include "nip044.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
static char* nip60_strdup(const char* s) {
if (!s) return NULL;
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, s, len + 1);
return out;
}
static const char* nip60_marker_to_string(nostr_nip60_ref_marker_t marker) {
switch (marker) {
case NOSTR_NIP60_REF_CREATED: return "created";
case NOSTR_NIP60_REF_DESTROYED: return "destroyed";
case NOSTR_NIP60_REF_REDEEMED: return "redeemed";
default: return "created";
}
}
static nostr_nip60_ref_marker_t nip60_string_to_marker(const char* s) {
if (!s) return NOSTR_NIP60_REF_CREATED;
if (strcmp(s, "created") == 0) return NOSTR_NIP60_REF_CREATED;
if (strcmp(s, "destroyed") == 0) return NOSTR_NIP60_REF_DESTROYED;
if (strcmp(s, "redeemed") == 0) return NOSTR_NIP60_REF_REDEEMED;
return NOSTR_NIP60_REF_CREATED;
}
static const char* nip60_direction_to_string(nostr_nip60_direction_t direction) {
return (direction == NOSTR_NIP60_DIRECTION_OUT) ? "out" : "in";
}
static nostr_nip60_direction_t nip60_string_to_direction(const char* s) {
if (s && strcmp(s, "out") == 0) return NOSTR_NIP60_DIRECTION_OUT;
return NOSTR_NIP60_DIRECTION_IN;
}
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) {
return NOSTR_ERROR_INVALID_INPUT;
}
unsigned char pubkey[32];
if (nostr_ec_public_key_from_private_key(private_key, pubkey) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return nostr_nip44_encrypt(private_key, pubkey, plaintext, output, output_size);
}
static int nip60_decrypt_event_content(cJSON* event,
const unsigned char* private_key,
char* output,
size_t output_size) {
if (!event || !private_key || !output || output_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
if (!pubkey_item || !content_item || !cJSON_IsString(pubkey_item) || !cJSON_IsString(content_item)) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
const char* pubkey_hex = cJSON_GetStringValue(pubkey_item);
const char* content = cJSON_GetStringValue(content_item);
if (!pubkey_hex || !content) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
unsigned char sender_pubkey[32];
if (nostr_hex_to_bytes(pubkey_hex, sender_pubkey, 32) != 0) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
int rc = nostr_nip44_decrypt(private_key, sender_pubkey, content, output, output_size);
if (rc != NOSTR_SUCCESS) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
return NOSTR_SUCCESS;
}
uint64_t nostr_nip60_sum_proofs(const nostr_cashu_proof_t* proofs, int proof_count) {
if (!proofs || proof_count <= 0) return 0;
uint64_t total = 0;
for (int i = 0; i < proof_count; i++) {
total += proofs[i].amount;
}
return total;
}
void nostr_nip60_free_proofs(nostr_cashu_proof_t* proofs, int proof_count) {
if (!proofs) return;
for (int i = 0; i < proof_count; i++) {
free(proofs[i].secret);
free(proofs[i].C);
proofs[i].secret = NULL;
proofs[i].C = NULL;
}
free(proofs);
}
cJSON* nostr_nip60_proofs_to_json(const nostr_cashu_proof_t* proofs, int proof_count) {
if (!proofs || proof_count < 0) return NULL;
cJSON* arr = cJSON_CreateArray();
if (!arr) return NULL;
for (int i = 0; i < proof_count; i++) {
cJSON* obj = cJSON_CreateObject();
if (!obj) {
cJSON_Delete(arr);
return NULL;
}
cJSON_AddStringToObject(obj, "id", proofs[i].id);
cJSON_AddNumberToObject(obj, "amount", (double)proofs[i].amount);
cJSON_AddStringToObject(obj, "secret", proofs[i].secret ? proofs[i].secret : "");
cJSON_AddStringToObject(obj, "C", proofs[i].C ? proofs[i].C : "");
cJSON_AddItemToArray(arr, obj);
}
return arr;
}
int nostr_nip60_proofs_from_json(cJSON* json_array,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out) {
if (!json_array || !cJSON_IsArray(json_array) || !proofs_out || !proof_count_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*proofs_out = NULL;
*proof_count_out = 0;
int count = cJSON_GetArraySize(json_array);
if (count <= 0) {
return NOSTR_SUCCESS;
}
nostr_cashu_proof_t* proofs = (nostr_cashu_proof_t*)calloc((size_t)count, sizeof(nostr_cashu_proof_t));
if (!proofs) {
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < count; i++) {
cJSON* p = cJSON_GetArrayItem(json_array, i);
if (!p || !cJSON_IsObject(p)) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
}
cJSON* id_item = cJSON_GetObjectItem(p, "id");
cJSON* amount_item = cJSON_GetObjectItem(p, "amount");
cJSON* secret_item = cJSON_GetObjectItem(p, "secret");
cJSON* c_item = cJSON_GetObjectItem(p, "C");
if (!id_item || !amount_item || !secret_item || !c_item ||
!cJSON_IsString(id_item) || !cJSON_IsNumber(amount_item) ||
!cJSON_IsString(secret_item) || !cJSON_IsString(c_item)) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
}
const char* id = cJSON_GetStringValue(id_item);
const char* secret = cJSON_GetStringValue(secret_item);
const char* C = cJSON_GetStringValue(c_item);
if (!id || !secret || !C) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
}
strncpy(proofs[i].id, id, sizeof(proofs[i].id) - 1);
proofs[i].id[sizeof(proofs[i].id) - 1] = '\0';
proofs[i].amount = (uint64_t)cJSON_GetNumberValue(amount_item);
proofs[i].secret = nip60_strdup(secret);
proofs[i].C = nip60_strdup(C);
if (!proofs[i].secret || !proofs[i].C) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
*proofs_out = proofs;
*proof_count_out = count;
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) {
return NULL;
}
cJSON* payload = cJSON_CreateArray();
if (!payload) return NULL;
cJSON* priv_row = cJSON_CreateArray();
if (!priv_row) {
cJSON_Delete(payload);
return NULL;
}
cJSON_AddItemToArray(priv_row, cJSON_CreateString("privkey"));
cJSON_AddItemToArray(priv_row, cJSON_CreateString(wallet_data->privkey));
cJSON_AddItemToArray(payload, priv_row);
for (int i = 0; i < wallet_data->mint_count; i++) {
if (!wallet_data->mint_urls || !wallet_data->mint_urls[i]) continue;
cJSON* mint_row = cJSON_CreateArray();
if (!mint_row) {
cJSON_Delete(payload);
return NULL;
}
cJSON_AddItemToArray(mint_row, cJSON_CreateString("mint"));
cJSON_AddItemToArray(mint_row, cJSON_CreateString(wallet_data->mint_urls[i]));
cJSON_AddItemToArray(payload, mint_row);
}
char* plain = cJSON_PrintUnformatted(payload);
cJSON_Delete(payload);
if (!plain) return NULL;
char encrypted[65536];
int rc = nip60_encrypt_self(private_key, 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);
}
int nostr_nip60_parse_wallet_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_wallet_data_t* wallet_data_out) {
if (!event || !private_key || !wallet_data_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(wallet_data_out, 0, sizeof(*wallet_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_WALLET_KIND) {
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
char decrypted[65536];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
cJSON* payload = cJSON_Parse(decrypted);
if (!payload || !cJSON_IsArray(payload)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
int mint_count = 0;
cJSON* row = NULL;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
cJSON* v = cJSON_GetArrayItem(row, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
if (key && strcmp(key, "mint") == 0) mint_count++;
}
if (mint_count > 0) {
wallet_data_out->mint_urls = (char**)calloc((size_t)mint_count, sizeof(char*));
if (!wallet_data_out->mint_urls) {
cJSON_Delete(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int mint_idx = 0;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
cJSON* v = cJSON_GetArrayItem(row, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
const char* val = cJSON_GetStringValue(v);
if (!key || !val) continue;
if (strcmp(key, "privkey") == 0) {
strncpy(wallet_data_out->privkey, val, sizeof(wallet_data_out->privkey) - 1);
wallet_data_out->privkey[sizeof(wallet_data_out->privkey) - 1] = '\0';
} else if (strcmp(key, "mint") == 0 && mint_idx < mint_count) {
wallet_data_out->mint_urls[mint_idx] = nip60_strdup(val);
if (!wallet_data_out->mint_urls[mint_idx]) {
cJSON_Delete(payload);
nostr_nip60_free_wallet_data(wallet_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
mint_idx++;
}
}
wallet_data_out->mint_count = mint_idx;
cJSON_Delete(payload);
if (wallet_data_out->privkey[0] == '\0' || wallet_data_out->mint_count == 0) {
nostr_nip60_free_wallet_data(wallet_data_out);
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
return NOSTR_SUCCESS;
}
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data) {
if (!data) return;
if (data->mint_urls) {
for (int i = 0; i < data->mint_count; i++) {
free(data->mint_urls[i]);
}
free(data->mint_urls);
}
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 ||
!token_data->proofs || token_data->proof_count <= 0) {
return NULL;
}
cJSON* payload = cJSON_CreateObject();
if (!payload) return NULL;
cJSON_AddStringToObject(payload, "mint", token_data->mint_url);
cJSON* proofs = nostr_nip60_proofs_to_json(token_data->proofs, token_data->proof_count);
if (!proofs) {
cJSON_Delete(payload);
return NULL;
}
cJSON_AddItemToObject(payload, "proofs", proofs);
if (token_data->deleted_token_ids && token_data->deleted_count > 0) {
cJSON* del = cJSON_CreateArray();
if (!del) {
cJSON_Delete(payload);
return NULL;
}
for (int i = 0; i < token_data->deleted_count; i++) {
if (token_data->deleted_token_ids[i]) {
cJSON_AddItemToArray(del, cJSON_CreateString(token_data->deleted_token_ids[i]));
}
}
cJSON_AddItemToObject(payload, "del", del);
}
char* plain = cJSON_PrintUnformatted(payload);
cJSON_Delete(payload);
if (!plain) return NULL;
char encrypted[131072];
int rc = nip60_encrypt_self(private_key, 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);
}
int nostr_nip60_parse_token_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_token_data_t* token_data_out) {
if (!event || !private_key || !token_data_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(token_data_out, 0, sizeof(*token_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_TOKEN_KIND) {
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
}
char decrypted[131072];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
cJSON* payload = cJSON_Parse(decrypted);
if (!payload || !cJSON_IsObject(payload)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
}
cJSON* mint_item = cJSON_GetObjectItem(payload, "mint");
cJSON* proofs_item = cJSON_GetObjectItem(payload, "proofs");
if (!mint_item || !proofs_item || !cJSON_IsString(mint_item) || !cJSON_IsArray(proofs_item)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
}
const char* mint_url = cJSON_GetStringValue(mint_item);
token_data_out->mint_url = nip60_strdup(mint_url ? mint_url : "");
if (!token_data_out->mint_url) {
cJSON_Delete(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
rc = nostr_nip60_proofs_from_json(proofs_item, &token_data_out->proofs, &token_data_out->proof_count);
if (rc != NOSTR_SUCCESS) {
cJSON_Delete(payload);
nostr_nip60_free_token_data(token_data_out);
return rc;
}
cJSON* del_item = cJSON_GetObjectItem(payload, "del");
if (del_item && cJSON_IsArray(del_item)) {
int del_count = cJSON_GetArraySize(del_item);
if (del_count > 0) {
token_data_out->deleted_token_ids = (char**)calloc((size_t)del_count, sizeof(char*));
if (!token_data_out->deleted_token_ids) {
cJSON_Delete(payload);
nostr_nip60_free_token_data(token_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < del_count; i++) {
cJSON* it = cJSON_GetArrayItem(del_item, i);
if (!it || !cJSON_IsString(it)) continue;
const char* s = cJSON_GetStringValue(it);
if (!s) continue;
token_data_out->deleted_token_ids[token_data_out->deleted_count] = nip60_strdup(s);
if (!token_data_out->deleted_token_ids[token_data_out->deleted_count]) {
cJSON_Delete(payload);
nostr_nip60_free_token_data(token_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
token_data_out->deleted_count++;
}
}
}
cJSON_Delete(payload);
return NOSTR_SUCCESS;
}
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data) {
if (!data) return;
free(data->mint_url);
nostr_nip60_free_proofs(data->proofs, data->proof_count);
if (data->deleted_token_ids) {
for (int i = 0; i < data->deleted_count; i++) {
free(data->deleted_token_ids[i]);
}
free(data->deleted_token_ids);
}
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* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(token_event_id));
cJSON_AddItemToArray(tags, e_tag);
cJSON* k_tag = cJSON_CreateArray();
cJSON_AddItemToArray(k_tag, cJSON_CreateString("k"));
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_Delete(tags);
return evt;
}
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_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(&token, private_key, timestamp);
}
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* payload = cJSON_CreateArray();
if (!payload) return NULL;
cJSON* dir = cJSON_CreateArray();
cJSON_AddItemToArray(dir, cJSON_CreateString("direction"));
cJSON_AddItemToArray(dir, cJSON_CreateString(nip60_direction_to_string(history_data->direction)));
cJSON_AddItemToArray(payload, dir);
char amount_buf[32];
snprintf(amount_buf, sizeof(amount_buf), "%llu", (unsigned long long)history_data->amount);
cJSON* amt = cJSON_CreateArray();
cJSON_AddItemToArray(amt, cJSON_CreateString("amount"));
cJSON_AddItemToArray(amt, cJSON_CreateString(amount_buf));
cJSON_AddItemToArray(payload, amt);
for (int i = 0; i < history_data->ref_count; i++) {
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].relay_hint));
cJSON_AddItemToArray(e, cJSON_CreateString(nip60_marker_to_string(history_data->refs[i].marker)));
cJSON_AddItemToArray(payload, e);
}
char* plain = cJSON_PrintUnformatted(payload);
cJSON_Delete(payload);
if (!plain) return NULL;
char encrypted[65536];
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
free(plain);
if (rc != NOSTR_SUCCESS) return NULL;
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
for (int i = 0; i < history_data->ref_count; i++) {
if (history_data->refs[i].marker != NOSTR_NIP60_REF_REDEEMED) continue;
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].relay_hint));
cJSON_AddItemToArray(e, cJSON_CreateString("redeemed"));
cJSON_AddItemToArray(tags, e);
}
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
int nostr_nip60_parse_history_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_history_data_t* history_data_out) {
if (!event || !private_key || !history_data_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(history_data_out, 0, sizeof(*history_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_HISTORY_KIND) {
return NOSTR_ERROR_NIP60_INVALID_HISTORY;
}
char decrypted[65536];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
cJSON* payload = cJSON_Parse(decrypted);
if (!payload || !cJSON_IsArray(payload)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_HISTORY;
}
int ref_count = 0;
cJSON* row = NULL;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
if (!k || !cJSON_IsString(k)) continue;
const char* key = cJSON_GetStringValue(k);
if (key && strcmp(key, "e") == 0) ref_count++;
}
if (ref_count > 0) {
history_data_out->refs = (nostr_nip60_history_ref_t*)calloc((size_t)ref_count, sizeof(nostr_nip60_history_ref_t));
if (!history_data_out->refs) {
cJSON_Delete(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int ref_idx = 0;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
cJSON* v = cJSON_GetArrayItem(row, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
const char* val = cJSON_GetStringValue(v);
if (!key || !val) continue;
if (strcmp(key, "direction") == 0) {
history_data_out->direction = nip60_string_to_direction(val);
} else if (strcmp(key, "amount") == 0) {
history_data_out->amount = (uint64_t)strtoull(val, NULL, 10);
} else if (strcmp(key, "e") == 0 && ref_idx < ref_count) {
strncpy(history_data_out->refs[ref_idx].event_id, val,
sizeof(history_data_out->refs[ref_idx].event_id) - 1);
cJSON* relay_item = cJSON_GetArrayItem(row, 2);
cJSON* marker_item = cJSON_GetArrayItem(row, 3);
if (relay_item && cJSON_IsString(relay_item)) {
const char* relay = cJSON_GetStringValue(relay_item);
if (relay) {
strncpy(history_data_out->refs[ref_idx].relay_hint, relay,
sizeof(history_data_out->refs[ref_idx].relay_hint) - 1);
}
}
if (marker_item && cJSON_IsString(marker_item)) {
const char* m = cJSON_GetStringValue(marker_item);
history_data_out->refs[ref_idx].marker = nip60_string_to_marker(m);
} else {
history_data_out->refs[ref_idx].marker = NOSTR_NIP60_REF_CREATED;
}
ref_idx++;
}
}
history_data_out->ref_count = ref_idx;
cJSON_Delete(payload);
return NOSTR_SUCCESS;
}
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data) {
if (!data) return;
free(data->refs);
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) {
return NULL;
}
char encrypted[4096];
int rc = nip60_encrypt_self(private_key, quote_id, encrypted, sizeof(encrypted));
if (rc != NOSTR_SUCCESS) return NULL;
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* exp_tag = cJSON_CreateArray();
cJSON_AddItemToArray(exp_tag, cJSON_CreateString("expiration"));
char exp_buf[32];
snprintf(exp_buf, sizeof(exp_buf), "%lld", (long long)expiration);
cJSON_AddItemToArray(exp_tag, cJSON_CreateString(exp_buf));
cJSON_AddItemToArray(tags, exp_tag);
cJSON* mint_tag = cJSON_CreateArray();
cJSON_AddItemToArray(mint_tag, cJSON_CreateString("mint"));
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_Delete(tags);
return evt;
}
int nostr_nip60_parse_quote_event(cJSON* event,
const unsigned char* private_key,
char* quote_id_out,
size_t quote_id_size,
char* mint_url_out,
size_t mint_url_size,
time_t* expiration_out) {
if (!event || !private_key || !quote_id_out || quote_id_size == 0 ||
!mint_url_out || mint_url_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_QUOTE_KIND) {
return NOSTR_ERROR_NIP60_INVALID_QUOTE;
}
char decrypted[4096];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
strncpy(quote_id_out, decrypted, quote_id_size - 1);
quote_id_out[quote_id_size - 1] = '\0';
mint_url_out[0] = '\0';
if (expiration_out) *expiration_out = 0;
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* k = cJSON_GetArrayItem(tag, 0);
cJSON* v = cJSON_GetArrayItem(tag, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
const char* val = cJSON_GetStringValue(v);
if (!key || !val) continue;
if (strcmp(key, "mint") == 0) {
strncpy(mint_url_out, val, mint_url_size - 1);
mint_url_out[mint_url_size - 1] = '\0';
} else if (strcmp(key, "expiration") == 0 && expiration_out) {
*expiration_out = (time_t)strtoll(val, NULL, 10);
}
}
}
return NOSTR_SUCCESS;
}
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex) {
if (!pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_WALLET_KIND));
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_TOKEN_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
return filter;
}
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since) {
if (!pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_HISTORY_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
if (since > 0) {
cJSON_AddNumberToObject(filter, "since", (double)since);
}
return filter;
}
+146
View File
@@ -0,0 +1,146 @@
/*
* NIP-60: Cashu Wallet
* https://github.com/nostr-protocol/nips/blob/master/60.md
*/
#ifndef NOSTR_NIP060_H
#define NOSTR_NIP060_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "nip001.h"
#include "nostr_common.h"
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NOSTR_NIP60_WALLET_KIND 17375
#define NOSTR_NIP60_TOKEN_KIND 7375
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 17
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
typedef struct {
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
uint64_t amount;
char* secret;
char* C;
} nostr_cashu_proof_t;
typedef struct {
char privkey[65];
char** mint_urls;
int mint_count;
} nostr_nip60_wallet_data_t;
typedef struct {
char* mint_url;
nostr_cashu_proof_t* proofs;
int proof_count;
char** deleted_token_ids;
int deleted_count;
} nostr_nip60_token_data_t;
typedef enum {
NOSTR_NIP60_DIRECTION_IN = 0,
NOSTR_NIP60_DIRECTION_OUT = 1
} nostr_nip60_direction_t;
typedef enum {
NOSTR_NIP60_REF_CREATED = 0,
NOSTR_NIP60_REF_DESTROYED = 1,
NOSTR_NIP60_REF_REDEEMED = 2
} nostr_nip60_ref_marker_t;
typedef struct {
char event_id[NOSTR_CASHU_EVENT_ID_HEX_SIZE];
char relay_hint[256];
nostr_nip60_ref_marker_t marker;
} nostr_nip60_history_ref_t;
typedef struct {
nostr_nip60_direction_t direction;
uint64_t amount;
nostr_nip60_history_ref_t* refs;
int ref_count;
} nostr_nip60_history_data_t;
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
const unsigned char* private_key,
time_t timestamp);
int nostr_nip60_parse_wallet_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_wallet_data_t* wallet_data_out);
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);
int nostr_nip60_parse_token_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_token_data_t* token_data_out);
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_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_history_event(const nostr_nip60_history_data_t* history_data,
const unsigned char* private_key,
time_t timestamp);
int nostr_nip60_parse_history_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_history_data_t* history_data_out);
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* 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);
int nostr_nip60_parse_quote_event(cJSON* event,
const unsigned char* private_key,
char* quote_id_out,
size_t quote_id_size,
char* mint_url_out,
size_t mint_url_size,
time_t* expiration_out);
uint64_t nostr_nip60_sum_proofs(const nostr_cashu_proof_t* proofs, int proof_count);
cJSON* nostr_nip60_proofs_to_json(const nostr_cashu_proof_t* proofs, int proof_count);
int nostr_nip60_proofs_from_json(cJSON* json_array,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out);
void nostr_nip60_free_proofs(nostr_cashu_proof_t* proofs, int proof_count);
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex);
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_NIP060_H */
+540
View File
@@ -0,0 +1,540 @@
/*
* NIP-61: Nutzaps Implementation
* https://github.com/nostr-protocol/nips/blob/master/61.md
*/
#include "nip061.h"
#include "nip060.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static char* nip61_strdup(const char* s) {
if (!s) return NULL;
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, s, len + 1);
return out;
}
static int nip61_mint_in_info(const char* mint_url, const nostr_nip61_nutzap_info_t* info) {
if (!mint_url || !info) return 0;
for (int i = 0; i < info->mint_count; i++) {
if (info->mints[i].url && strcmp(info->mints[i].url, mint_url) == 0) {
return 1;
}
}
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') {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
for (int i = 0; i < info->relay_count; i++) {
if (!info->relay_urls || !info->relay_urls[i]) continue;
cJSON* relay = cJSON_CreateArray();
cJSON_AddItemToArray(relay, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay, cJSON_CreateString(info->relay_urls[i]));
cJSON_AddItemToArray(tags, relay);
}
for (int i = 0; i < info->mint_count; i++) {
if (!info->mints || !info->mints[i].url) continue;
cJSON* mint = cJSON_CreateArray();
cJSON_AddItemToArray(mint, cJSON_CreateString("mint"));
cJSON_AddItemToArray(mint, cJSON_CreateString(info->mints[i].url));
for (int u = 0; u < info->mints[i].unit_count; u++) {
if (info->mints[i].units && info->mints[i].units[u]) {
cJSON_AddItemToArray(mint, cJSON_CreateString(info->mints[i].units[u]));
}
}
cJSON_AddItemToArray(tags, mint);
}
cJSON* pub = cJSON_CreateArray();
cJSON_AddItemToArray(pub, cJSON_CreateString("pubkey"));
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_Delete(tags);
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;
memset(info_out, 0, sizeof(*info_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP61_NUTZAP_INFO_KIND ||
!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_NIP61_INVALID_INFO;
}
int relay_count = 0;
int mint_count = 0;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
if (!t0 || !cJSON_IsString(t0)) continue;
const char* key = cJSON_GetStringValue(t0);
if (!key) continue;
if (strcmp(key, "relay") == 0) relay_count++;
if (strcmp(key, "mint") == 0) mint_count++;
}
if (relay_count > 0) {
info_out->relay_urls = (char**)calloc((size_t)relay_count, sizeof(char*));
if (!info_out->relay_urls) return NOSTR_ERROR_MEMORY_FAILED;
}
if (mint_count > 0) {
info_out->mints = (nostr_nip61_mint_entry_t*)calloc((size_t)mint_count, sizeof(nostr_nip61_mint_entry_t));
if (!info_out->mints) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int relay_idx = 0;
int mint_idx = 0;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1)) continue;
const char* key = cJSON_GetStringValue(t0);
const char* val = cJSON_GetStringValue(t1);
if (!key || !val) continue;
if (strcmp(key, "relay") == 0 && relay_idx < relay_count) {
info_out->relay_urls[relay_idx] = nip61_strdup(val);
if (!info_out->relay_urls[relay_idx]) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
relay_idx++;
} else if (strcmp(key, "mint") == 0 && mint_idx < mint_count) {
info_out->mints[mint_idx].url = nip61_strdup(val);
if (!info_out->mints[mint_idx].url) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
int total = cJSON_GetArraySize(tag);
int unit_count = (total > 2) ? (total - 2) : 0;
if (unit_count > 0) {
info_out->mints[mint_idx].units = (char**)calloc((size_t)unit_count, sizeof(char*));
if (!info_out->mints[mint_idx].units) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int u = 0; u < unit_count; u++) {
cJSON* unit_item = cJSON_GetArrayItem(tag, u + 2);
if (unit_item && cJSON_IsString(unit_item)) {
const char* us = cJSON_GetStringValue(unit_item);
if (us) {
info_out->mints[mint_idx].units[info_out->mints[mint_idx].unit_count] = nip61_strdup(us);
if (!info_out->mints[mint_idx].units[info_out->mints[mint_idx].unit_count]) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
info_out->mints[mint_idx].unit_count++;
}
}
}
}
mint_idx++;
} else if (strcmp(key, "pubkey") == 0) {
strncpy(info_out->pubkey, val, sizeof(info_out->pubkey) - 1);
info_out->pubkey[sizeof(info_out->pubkey) - 1] = '\0';
}
}
info_out->relay_count = relay_idx;
info_out->mint_count = mint_idx;
if (info_out->pubkey[0] == '\0' || info_out->mint_count == 0) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_NIP61_INVALID_INFO;
}
return NOSTR_SUCCESS;
}
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info) {
if (!info) return;
if (info->relay_urls) {
for (int i = 0; i < info->relay_count; i++) {
free(info->relay_urls[i]);
}
free(info->relay_urls);
}
if (info->mints) {
for (int i = 0; i < info->mint_count; i++) {
free(info->mints[i].url);
if (info->mints[i].units) {
for (int u = 0; u < info->mints[i].unit_count; u++) {
free(info->mints[i].units[u]);
}
free(info->mints[i].units);
}
}
free(info->mints);
}
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 ||
!nutzap_data->proofs || nutzap_data->proof_count <= 0 ||
nutzap_data->recipient_pubkey[0] == '\0') {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
for (int i = 0; i < nutzap_data->proof_count; i++) {
cJSON* proof_json = cJSON_CreateObject();
cJSON_AddStringToObject(proof_json, "id", nutzap_data->proofs[i].id);
cJSON_AddNumberToObject(proof_json, "amount", (double)nutzap_data->proofs[i].amount);
cJSON_AddStringToObject(proof_json, "secret", nutzap_data->proofs[i].secret ? nutzap_data->proofs[i].secret : "");
cJSON_AddStringToObject(proof_json, "C", nutzap_data->proofs[i].C ? nutzap_data->proofs[i].C : "");
char* proof_str = cJSON_PrintUnformatted(proof_json);
cJSON_Delete(proof_json);
if (!proof_str) {
cJSON_Delete(tags);
return NULL;
}
cJSON* proof_tag = cJSON_CreateArray();
cJSON_AddItemToArray(proof_tag, cJSON_CreateString("proof"));
cJSON_AddItemToArray(proof_tag, cJSON_CreateString(proof_str));
cJSON_AddItemToArray(tags, proof_tag);
free(proof_str);
}
cJSON* u = cJSON_CreateArray();
cJSON_AddItemToArray(u, cJSON_CreateString("u"));
cJSON_AddItemToArray(u, cJSON_CreateString(nutzap_data->mint_url));
cJSON_AddItemToArray(tags, u);
cJSON* p = cJSON_CreateArray();
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
cJSON_AddItemToArray(p, cJSON_CreateString(nutzap_data->recipient_pubkey));
cJSON_AddItemToArray(tags, p);
if (nutzap_data->nutzapped_event_id[0] != '\0') {
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_data->nutzapped_event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_data->nutzapped_relay_hint));
cJSON_AddItemToArray(tags, e);
if (nutzap_data->nutzapped_kind > 0) {
cJSON* k = cJSON_CreateArray();
char kind_buf[16];
snprintf(kind_buf, sizeof(kind_buf), "%d", nutzap_data->nutzapped_kind);
cJSON_AddItemToArray(k, cJSON_CreateString("k"));
cJSON_AddItemToArray(k, cJSON_CreateString(kind_buf));
cJSON_AddItemToArray(tags, k);
}
}
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_Delete(tags);
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;
memset(nutzap_data_out, 0, sizeof(*nutzap_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags = cJSON_GetObjectItem(event, "tags");
cJSON* content = cJSON_GetObjectItem(event, "content");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP61_NUTZAP_KIND ||
!tags || !cJSON_IsArray(tags) || !content || !cJSON_IsString(content)) {
return NOSTR_ERROR_NIP61_INVALID_NUTZAP;
}
const char* content_str = cJSON_GetStringValue(content);
nutzap_data_out->content = nip61_strdup(content_str ? content_str : "");
if (!nutzap_data_out->content) return NOSTR_ERROR_MEMORY_FAILED;
int proof_count = 0;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
if (t0 && cJSON_IsString(t0) && strcmp(cJSON_GetStringValue(t0), "proof") == 0) {
proof_count++;
}
}
if (proof_count > 0) {
nutzap_data_out->proofs = (nostr_cashu_proof_t*)calloc((size_t)proof_count, sizeof(nostr_cashu_proof_t));
if (!nutzap_data_out->proofs) {
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int proof_idx = 0;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1)) continue;
const char* key = cJSON_GetStringValue(t0);
const char* val = cJSON_GetStringValue(t1);
if (!key || !val) continue;
if (strcmp(key, "proof") == 0 && proof_idx < proof_count) {
cJSON* proof_obj = cJSON_Parse(val);
if (!proof_obj || !cJSON_IsObject(proof_obj)) {
cJSON_Delete(proof_obj);
continue;
}
cJSON* id = cJSON_GetObjectItem(proof_obj, "id");
cJSON* amount = cJSON_GetObjectItem(proof_obj, "amount");
cJSON* secret = cJSON_GetObjectItem(proof_obj, "secret");
cJSON* C = cJSON_GetObjectItem(proof_obj, "C");
if (id && cJSON_IsString(id) && amount && cJSON_IsNumber(amount) &&
secret && cJSON_IsString(secret) && C && cJSON_IsString(C)) {
const char* id_s = cJSON_GetStringValue(id);
const char* sec_s = cJSON_GetStringValue(secret);
const char* c_s = cJSON_GetStringValue(C);
if (id_s && sec_s && c_s) {
strncpy(nutzap_data_out->proofs[proof_idx].id, id_s,
sizeof(nutzap_data_out->proofs[proof_idx].id) - 1);
nutzap_data_out->proofs[proof_idx].amount = (uint64_t)cJSON_GetNumberValue(amount);
nutzap_data_out->proofs[proof_idx].secret = nip61_strdup(sec_s);
nutzap_data_out->proofs[proof_idx].C = nip61_strdup(c_s);
if (!nutzap_data_out->proofs[proof_idx].secret || !nutzap_data_out->proofs[proof_idx].C) {
cJSON_Delete(proof_obj);
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
proof_idx++;
}
}
cJSON_Delete(proof_obj);
} else if (strcmp(key, "u") == 0) {
free(nutzap_data_out->mint_url);
nutzap_data_out->mint_url = nip61_strdup(val);
if (!nutzap_data_out->mint_url) {
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
} else if (strcmp(key, "p") == 0) {
strncpy(nutzap_data_out->recipient_pubkey, val, sizeof(nutzap_data_out->recipient_pubkey) - 1);
} else if (strcmp(key, "e") == 0) {
strncpy(nutzap_data_out->nutzapped_event_id, val, sizeof(nutzap_data_out->nutzapped_event_id) - 1);
cJSON* t2 = cJSON_GetArrayItem(tag, 2);
if (t2 && cJSON_IsString(t2)) {
const char* relay = cJSON_GetStringValue(t2);
if (relay) strncpy(nutzap_data_out->nutzapped_relay_hint, relay,
sizeof(nutzap_data_out->nutzapped_relay_hint) - 1);
}
} else if (strcmp(key, "k") == 0) {
nutzap_data_out->nutzapped_kind = atoi(val);
}
}
nutzap_data_out->proof_count = proof_idx;
if (!nutzap_data_out->mint_url || nutzap_data_out->recipient_pubkey[0] == '\0' || proof_idx == 0) {
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_NIP61_INVALID_NUTZAP;
}
return NOSTR_SUCCESS;
}
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data) {
if (!data) return;
free(data->content);
free(data->mint_url);
nostr_nip60_free_proofs(data->proofs, data->proof_count);
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) {
return NULL;
}
nostr_nip60_history_ref_t refs[1];
memset(refs, 0, sizeof(refs));
strncpy(refs[0].event_id, created_token_event_id, sizeof(refs[0].event_id) - 1);
if (created_token_relay_hint) {
strncpy(refs[0].relay_hint, created_token_relay_hint, sizeof(refs[0].relay_hint) - 1);
}
refs[0].marker = NOSTR_NIP60_REF_CREATED;
nostr_nip60_history_data_t hist;
memset(&hist, 0, sizeof(hist));
hist.direction = NOSTR_NIP60_DIRECTION_IN;
hist.amount = amount;
hist.refs = refs;
hist.ref_count = 1;
cJSON* evt = nostr_nip60_create_history_event(&hist, private_key, timestamp);
if (!evt) return NULL;
cJSON* tags = cJSON_GetObjectItem(evt, "tags");
if (!tags || !cJSON_IsArray(tags)) return evt;
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_relay_hint ? nutzap_relay_hint : ""));
cJSON_AddItemToArray(e, cJSON_CreateString("redeemed"));
cJSON_AddItemToArray(tags, e);
cJSON* p = cJSON_CreateArray();
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
cJSON_AddItemToArray(p, cJSON_CreateString(sender_pubkey));
cJSON_AddItemToArray(tags, p);
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;
nostr_nip61_nutzap_data_t nutzap;
memset(&nutzap, 0, sizeof(nutzap));
int rc = nostr_nip61_parse_nutzap_event(nutzap_event, &nutzap);
if (rc != NOSTR_SUCCESS) return rc;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
rc = nostr_nip61_parse_nutzap_info_event(nutzap_info_event, &info);
if (rc != NOSTR_SUCCESS) {
nostr_nip61_free_nutzap_data(&nutzap);
return rc;
}
if (!nip61_mint_in_info(nutzap.mint_url, &info)) {
nostr_nip61_free_nutzap_data(&nutzap);
nostr_nip61_free_nutzap_info(&info);
return NOSTR_ERROR_NIP61_MINT_MISMATCH;
}
int key_match = 0;
if (strlen(info.pubkey) == 64) {
for (int i = 0; i < nutzap.proof_count; i++) {
if (nutzap.proofs[i].secret && strstr(nutzap.proofs[i].secret, info.pubkey) != NULL) {
key_match = 1;
break;
}
}
} else if (strlen(info.pubkey) == 66 && strncmp(info.pubkey, "02", 2) == 0) {
const char* xonly = info.pubkey + 2;
for (int i = 0; i < nutzap.proof_count; i++) {
if (nutzap.proofs[i].secret && strstr(nutzap.proofs[i].secret, xonly) != NULL) {
key_match = 1;
break;
}
}
}
nostr_nip61_free_nutzap_data(&nutzap);
nostr_nip61_free_nutzap_info(&info);
if (!key_match) {
return NOSTR_ERROR_NIP61_PUBKEY_MISMATCH;
}
return NOSTR_SUCCESS;
}
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex) {
if (!pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP61_NUTZAP_INFO_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
return filter;
}
cJSON* nostr_nip61_create_nutzap_filter(const char* recipient_pubkey_hex,
const char** mint_urls,
int mint_count,
time_t since) {
if (!recipient_pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP61_NUTZAP_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* pvals = cJSON_CreateArray();
cJSON_AddItemToArray(pvals, cJSON_CreateString(recipient_pubkey_hex));
cJSON_AddItemToObject(filter, "#p", pvals);
if (mint_urls && mint_count > 0) {
cJSON* uvals = cJSON_CreateArray();
for (int i = 0; i < mint_count; i++) {
if (mint_urls[i]) cJSON_AddItemToArray(uvals, cJSON_CreateString(mint_urls[i]));
}
cJSON_AddItemToObject(filter, "#u", uvals);
}
if (since > 0) {
cJSON_AddNumberToObject(filter, "since", (double)since);
}
return filter;
}
+89
View File
@@ -0,0 +1,89 @@
/*
* NIP-61: Nutzaps
* https://github.com/nostr-protocol/nips/blob/master/61.md
*/
#ifndef NOSTR_NIP061_H
#define NOSTR_NIP061_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "nip001.h"
#include "nip060.h"
#include "nostr_common.h"
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
#define NOSTR_NIP61_NUTZAP_KIND 9321
typedef struct {
char* url;
char** units;
int unit_count;
} nostr_nip61_mint_entry_t;
typedef struct {
char** relay_urls;
int relay_count;
nostr_nip61_mint_entry_t* mints;
int mint_count;
char pubkey[NOSTR_CASHU_PUBKEY_HEX_SIZE];
} nostr_nip61_nutzap_info_t;
typedef struct {
char* content;
nostr_cashu_proof_t* proofs;
int proof_count;
char* mint_url;
char recipient_pubkey[65];
char nutzapped_event_id[NOSTR_CASHU_EVENT_ID_HEX_SIZE];
char nutzapped_relay_hint[256];
int nutzapped_kind;
} nostr_nip61_nutzap_data_t;
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
const unsigned char* private_key,
time_t timestamp);
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
nostr_nip61_nutzap_info_t* info_out);
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);
int nostr_nip61_parse_nutzap_event(cJSON* event,
nostr_nip61_nutzap_data_t* nutzap_data_out);
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* 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);
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event);
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
cJSON* nostr_nip61_create_nutzap_filter(const char* recipient_pubkey_hex,
const char** mint_urls,
int mint_count,
time_t since);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_NIP061_H */
-53
View File
@@ -1,53 +0,0 @@
#ifndef _NOSTR_AES_H_
#define _NOSTR_AES_H_
#include <stdint.h>
#include <stddef.h>
// Configure for NIP-04 requirements: AES-256-CBC only
#define CBC 1
#define ECB 0
#define CTR 0
// Configure for AES-256 (required by NIP-04)
#define AES128 0
#define AES192 0
#define AES256 1
#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only
#if defined(AES256) && (AES256 == 1)
#define AES_KEYLEN 32
#define AES_keyExpSize 240
#elif defined(AES192) && (AES192 == 1)
#define AES_KEYLEN 24
#define AES_keyExpSize 208
#else
#define AES_KEYLEN 16 // Key length in bytes
#define AES_keyExpSize 176
#endif
struct AES_ctx
{
uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif
#if defined(CBC) && (CBC == 1)
// buffer size MUST be multiple of AES_BLOCKLEN;
// Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme
// NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv()
// no IV should ever be reused with the same key
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CBC) && (CBC == 1)
#endif // _NOSTR_AES_H_
Binary file not shown.
-115
View File
@@ -1,115 +0,0 @@
/*
* nostr_chacha20.h - ChaCha20 stream cipher implementation
*
* Implementation based on RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols"
*
* This is a small, portable implementation for NIP-44 support in the NOSTR library.
* The implementation prioritizes correctness and simplicity over performance.
*/
#ifndef NOSTR_CHACHA20_H
#define NOSTR_CHACHA20_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* ============================================================================
* CONSTANTS AND DEFINITIONS
* ============================================================================
*/
#define CHACHA20_KEY_SIZE 32 /* 256 bits */
#define CHACHA20_NONCE_SIZE 12 /* 96 bits */
#define CHACHA20_BLOCK_SIZE 64 /* 512 bits */
/*
* ============================================================================
* CORE CHACHA20 FUNCTIONS
* ============================================================================
*/
/**
* ChaCha20 quarter round operation
*
* Operates on four 32-bit words performing the core ChaCha20 quarter round:
* a += b; d ^= a; d <<<= 16;
* c += d; b ^= c; b <<<= 12;
* a += b; d ^= a; d <<<= 8;
* c += d; b ^= c; b <<<= 7;
*
* @param state[in,out] ChaCha state as 16 32-bit words
* @param a, b, c, d Indices into state array for quarter round
*/
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d);
/**
* ChaCha20 block function
*
* Transforms a 64-byte input block using ChaCha20 algorithm with 20 rounds.
*
* @param key[in] 32-byte key
* @param counter[in] 32-bit block counter
* @param nonce[in] 12-byte nonce
* @param output[out] 64-byte output buffer
* @return 0 on success, negative on error
*/
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]);
/**
* ChaCha20 encryption/decryption
*
* Encrypts or decrypts data using ChaCha20 stream cipher.
* Since ChaCha20 is a stream cipher, encryption and decryption are the same operation.
*
* @param key[in] 32-byte key
* @param counter[in] Initial 32-bit counter value
* @param nonce[in] 12-byte nonce
* @param input[in] Input data to encrypt/decrypt
* @param output[out] Output buffer (can be same as input)
* @param length[in] Length of input data in bytes
* @return 0 on success, negative on error
*/
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);
/*
* ============================================================================
* UTILITY FUNCTIONS
* ============================================================================
*/
/**
* Initialize ChaCha20 state matrix
*
* Sets up the initial 16-word state matrix with constants, key, counter, and nonce.
*
* @param state[out] 16-word state array to initialize
* @param key[in] 32-byte key
* @param counter[in] 32-bit block counter
* @param nonce[in] 12-byte nonce
*/
void chacha20_init_state(uint32_t state[16], const uint8_t key[32],
uint32_t counter, const uint8_t nonce[12]);
/**
* Serialize ChaCha20 state to bytes
*
* Converts 16 32-bit words to 64 bytes in little-endian format.
*
* @param state[in] 16-word state array
* @param output[out] 64-byte output buffer
*/
void chacha20_serialize_state(const uint32_t state[16], uint8_t output[64]);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_CHACHA20_H */
Binary file not shown.
+106
View File
@@ -0,0 +1,106 @@
/*
* NOSTR Core Library - Common Utilities
*
* Common functions and utilities shared across the library
*/
#include "nostr_common.h"
#include "utils.h"
/**
* Convert error code to human-readable string
* Handles all error codes defined in nostr_common.h
*/
const char* nostr_strerror(int error_code) {
switch (error_code) {
case NOSTR_SUCCESS: return "Success";
case NOSTR_ERROR_INVALID_INPUT: return "Invalid input";
case NOSTR_ERROR_CRYPTO_FAILED: return "Cryptographic operation failed";
case NOSTR_ERROR_MEMORY_FAILED: return "Memory allocation failed";
case NOSTR_ERROR_IO_FAILED: return "I/O operation failed";
case NOSTR_ERROR_NETWORK_FAILED: return "Network operation failed";
case NOSTR_ERROR_NIP04_INVALID_FORMAT: return "NIP-04 invalid format";
case NOSTR_ERROR_NIP04_DECRYPT_FAILED: return "NIP-04 decryption failed";
case NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL: return "NIP-04 buffer too small";
case NOSTR_ERROR_NIP44_INVALID_FORMAT: return "NIP-44: Invalid format";
case NOSTR_ERROR_NIP44_DECRYPT_FAILED: return "NIP-44: Decryption failed";
case NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL: return "NIP-44: Buffer too small";
case NOSTR_ERROR_NIP05_INVALID_IDENTIFIER: return "NIP-05: Invalid identifier format";
case NOSTR_ERROR_NIP05_HTTP_FAILED: return "NIP-05: HTTP request failed";
case NOSTR_ERROR_NIP05_JSON_PARSE_FAILED: return "NIP-05: JSON parsing failed";
case NOSTR_ERROR_NIP05_NAME_NOT_FOUND: return "NIP-05: Name not found in .well-known";
case NOSTR_ERROR_NIP05_PUBKEY_MISMATCH: return "NIP-05: Public key mismatch";
case NOSTR_ERROR_EVENT_INVALID_STRUCTURE: return "Event has invalid structure";
case NOSTR_ERROR_EVENT_INVALID_ID: return "Event has invalid ID";
case NOSTR_ERROR_EVENT_INVALID_PUBKEY: return "Event has invalid public key";
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: return "Event has invalid signature";
case NOSTR_ERROR_EVENT_INVALID_CREATED_AT: return "Event has invalid timestamp";
case NOSTR_ERROR_EVENT_INVALID_KIND: return "Event has invalid kind";
case NOSTR_ERROR_EVENT_INVALID_TAGS: return "Event has invalid tags";
case NOSTR_ERROR_EVENT_INVALID_CONTENT: return "Event has invalid content";
case NOSTR_ERROR_NIP13_INSUFFICIENT: return "NIP-13: Insufficient PoW difficulty";
case NOSTR_ERROR_NIP13_NO_NONCE_TAG: return "NIP-13: Missing nonce tag";
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: return "NIP-13: Invalid nonce tag format";
case NOSTR_ERROR_NIP13_TARGET_MISMATCH: return "NIP-13: Target difficulty mismatch";
case NOSTR_ERROR_NIP13_CALCULATION: return "NIP-13: PoW calculation error";
case NOSTR_ERROR_NIP42_INVALID_CHALLENGE: return "NIP-42: Invalid challenge";
case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: return "NIP-42: Challenge expired";
case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID: return "NIP-42: Authentication event invalid";
case NOSTR_ERROR_NIP42_URL_MISMATCH: return "NIP-42: Relay URL mismatch";
case NOSTR_ERROR_NIP42_TIME_TOLERANCE: return "NIP-42: Timestamp outside tolerance";
case NOSTR_ERROR_NIP42_NOT_AUTHENTICATED: return "NIP-42: Client not authenticated";
case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format";
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT: return "NIP-42: Challenge too short";
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long";
case NOSTR_ERROR_NIP46_INVALID_BUNKER_URL: return "NIP-46: Invalid bunker URL";
case NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT: return "NIP-46: Invalid nostrconnect URL";
case NOSTR_ERROR_NIP46_INVALID_REQUEST: return "NIP-46: Invalid request payload";
case NOSTR_ERROR_NIP46_INVALID_RESPONSE: return "NIP-46: Invalid response payload";
case NOSTR_ERROR_NIP46_ENCRYPTION_FAILED: return "NIP-46: Encryption failed";
case NOSTR_ERROR_NIP46_DECRYPTION_FAILED: return "NIP-46: Decryption failed";
case NOSTR_ERROR_NIP46_CONNECTION_FAILED: return "NIP-46: Connection failed";
case NOSTR_ERROR_NIP46_TIMEOUT: return "NIP-46: Timeout";
case NOSTR_ERROR_NIP46_SECRET_MISMATCH: return "NIP-46: Secret mismatch";
case NOSTR_ERROR_NIP46_UNKNOWN_METHOD: return "NIP-46: Unknown method";
case NOSTR_ERROR_NIP46_AUTH_CHALLENGE: return "NIP-46: Auth challenge required";
case NOSTR_ERROR_NIP46_NOT_CONNECTED: return "NIP-46: Not connected";
case NOSTR_ERROR_NIP60_INVALID_WALLET: return "NIP-60: Invalid wallet event";
case NOSTR_ERROR_NIP60_INVALID_TOKEN: return "NIP-60: Invalid token event";
case NOSTR_ERROR_NIP60_INVALID_HISTORY: return "NIP-60: Invalid history event";
case NOSTR_ERROR_NIP60_INVALID_QUOTE: return "NIP-60: Invalid quote event";
case NOSTR_ERROR_NIP60_DECRYPT_FAILED: return "NIP-60: Decryption failed";
case NOSTR_ERROR_NIP60_INVALID_PROOFS: return "NIP-60: Invalid proofs payload";
case NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS: return "NIP-60: Insufficient funds";
case NOSTR_ERROR_NIP61_INVALID_INFO: return "NIP-61: Invalid info event";
case NOSTR_ERROR_NIP61_INVALID_NUTZAP: return "NIP-61: Invalid nutzap event";
case NOSTR_ERROR_NIP61_MINT_MISMATCH: return "NIP-61: Mint mismatch";
case NOSTR_ERROR_NIP61_PUBKEY_MISMATCH: return "NIP-61: Pubkey mismatch";
case NOSTR_ERROR_NIP61_VERIFICATION_FAILED: return "NIP-61: Verification failed";
case NOSTR_ERROR_CASHU_HTTP_FAILED: return "Cashu: HTTP request failed";
case NOSTR_ERROR_CASHU_JSON_PARSE_FAILED: return "Cashu: JSON parsing failed";
case NOSTR_ERROR_CASHU_MINT_ERROR: return "Cashu: Mint returned an error";
case NOSTR_ERROR_CASHU_QUOTE_NOT_PAID: return "Cashu: Quote not paid";
case NOSTR_ERROR_CASHU_QUOTE_EXPIRED: return "Cashu: Quote expired";
case NOSTR_ERROR_CASHU_PROOFS_SPENT: return "Cashu: One or more proofs are already spent";
case NOSTR_ERROR_CASHU_CRYPTO_FAILED: return "Cashu: Cryptographic operation failed";
case NOSTR_ERROR_CASHU_INVALID_KEYSET: return "Cashu: Invalid keyset";
default: return "Unknown error";
}
}
/**
* Initialize the NOSTR library
*/
int nostr_init(void) {
if (nostr_crypto_init() != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
/**
* Cleanup the NOSTR library
*/
void nostr_cleanup(void) {
nostr_crypto_cleanup();
}
+131
View File
@@ -0,0 +1,131 @@
/*
* NOSTR Core - Common Definitions
* Shared error constants and basic types for the modular NOSTR library
*/
#ifndef NOSTR_COMMON_H
#define NOSTR_COMMON_H
#include <stddef.h>
#include <stdint.h>
// Return codes
#define NOSTR_SUCCESS 0
#define NOSTR_ERROR_INVALID_INPUT -1
#define NOSTR_ERROR_CRYPTO_FAILED -2
#define NOSTR_ERROR_MEMORY_FAILED -3
#define NOSTR_ERROR_IO_FAILED -4
#define NOSTR_ERROR_NETWORK_FAILED -5
#define NOSTR_ERROR_NIP04_INVALID_FORMAT -10
#define NOSTR_ERROR_NIP04_DECRYPT_FAILED -11
#define NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL -12
#define NOSTR_ERROR_NIP44_INVALID_FORMAT -13
#define NOSTR_ERROR_NIP44_DECRYPT_FAILED -14
#define NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL -15
#define NOSTR_ERROR_NIP05_INVALID_IDENTIFIER -16
#define NOSTR_ERROR_NIP05_HTTP_FAILED -17
#define NOSTR_ERROR_NIP05_JSON_PARSE_FAILED -18
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
#define NOSTR_ERROR_EVENT_INVALID_ID -31
#define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32
#define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33
#define NOSTR_ERROR_EVENT_INVALID_CREATED_AT -34
#define NOSTR_ERROR_EVENT_INVALID_KIND -35
#define NOSTR_ERROR_EVENT_INVALID_TAGS -36
#define NOSTR_ERROR_EVENT_INVALID_CONTENT -37
// Authentication Rules System Error Codes
#define NOSTR_ERROR_AUTH_RULES_DISABLED -50
#define NOSTR_ERROR_AUTH_RULES_DENIED -51
#define NOSTR_ERROR_AUTH_RULES_DB_FAILED -52
#define NOSTR_ERROR_AUTH_RULES_INVALID_RULE -53
#define NOSTR_ERROR_AUTH_RULES_CACHE_FAILED -54
#define NOSTR_ERROR_AUTH_RULES_BACKEND_NOT_FOUND -55
// NIP-13 PoW-specific error codes
#define NOSTR_ERROR_NIP13_INSUFFICIENT -100
#define NOSTR_ERROR_NIP13_NO_NONCE_TAG -101
#define NOSTR_ERROR_NIP13_INVALID_NONCE_TAG -102
#define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103
#define NOSTR_ERROR_NIP13_CALCULATION -104
// NIP-42 Authentication-specific error codes
#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -200
#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201
#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -202
#define NOSTR_ERROR_NIP42_URL_MISMATCH -203
#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204
#define NOSTR_ERROR_NIP42_NOT_AUTHENTICATED -205
#define NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT -206
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
// NIP-46 Remote Signing error codes
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
#define NOSTR_ERROR_NIP46_TIMEOUT -307
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
// NIP-60 Cashu Wallet error codes
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
// NIP-61 Nutzap error codes
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
// Cashu Mint client error codes
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
// Constants
#define NOSTR_PRIVATE_KEY_SIZE 32
#define NOSTR_PUBLIC_KEY_SIZE 32
#define NOSTR_HEX_KEY_SIZE 65 // 64 + null terminator
#define NOSTR_BECH32_KEY_SIZE 100
#define NOSTR_MAX_CONTENT_SIZE 2048
#define NOSTR_MAX_URL_SIZE 256
#define NIP05_DEFAULT_TIMEOUT 10
// NIP-04 Constants
#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB
#define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
// NIP-44 Constants
#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65535 // 64KB - 1 (NIP-44 spec compliant)
// Forward declaration for cJSON (to avoid requiring cJSON.h in header)
struct cJSON;
// Function declarations
const char* nostr_strerror(int error_code);
// Library initialization functions
int nostr_init(void);
void nostr_cleanup(void);
#endif // NOSTR_COMMON_H
+328 -657
View File
File diff suppressed because it is too large Load Diff
-186
View File
@@ -1,186 +0,0 @@
/*
* NOSTR Crypto - Self-contained cryptographic functions
*
* Embedded implementations of crypto primitives needed for NOSTR
* No external dependencies except standard C library
*/
#ifndef NOSTR_CRYPTO_H
#define NOSTR_CRYPTO_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// =============================================================================
// CORE CRYPTO FUNCTIONS
// =============================================================================
// Initialize crypto subsystem
int nostr_crypto_init(void);
// Cleanup crypto subsystem
void nostr_crypto_cleanup(void);
// SHA-256 hash function
int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash);
// HMAC-SHA256
int nostr_hmac_sha256(const unsigned char* key, size_t key_len,
const unsigned char* data, size_t data_len,
unsigned char* output);
// HMAC-SHA512
int nostr_hmac_sha512(const unsigned char* key, size_t key_len,
const unsigned char* data, size_t data_len,
unsigned char* output);
// PBKDF2 with HMAC-SHA512
int nostr_pbkdf2_hmac_sha512(const unsigned char* password, size_t password_len,
const unsigned char* salt, size_t salt_len,
int iterations,
unsigned char* output, size_t output_len);
// SHA-512 implementation (for testing)
int nostr_sha512(const unsigned char* data, size_t len, unsigned char* hash);
// =============================================================================
// SECP256K1 ELLIPTIC CURVE FUNCTIONS
// =============================================================================
// Verify private key is valid
int nostr_ec_private_key_verify(const unsigned char* private_key);
// Generate public key from private key
int nostr_ec_public_key_from_private_key(const unsigned char* private_key,
unsigned char* public_key);
// Sign data with ECDSA
int nostr_ec_sign(const unsigned char* private_key,
const unsigned char* hash,
unsigned char* signature);
// RFC 6979 deterministic nonce generation
int nostr_rfc6979_generate_k(const unsigned char* private_key,
const unsigned char* message_hash,
unsigned char* k_out);
// =============================================================================
// HKDF KEY DERIVATION FUNCTIONS
// =============================================================================
// HKDF Extract step
int nostr_hkdf_extract(const unsigned char* salt, size_t salt_len,
const unsigned char* ikm, size_t ikm_len,
unsigned char* prk);
// HKDF Expand step
int nostr_hkdf_expand(const unsigned char* prk, size_t prk_len,
const unsigned char* info, size_t info_len,
unsigned char* okm, size_t okm_len);
// HKDF (Extract + Expand)
int nostr_hkdf(const unsigned char* salt, size_t salt_len,
const unsigned char* ikm, size_t ikm_len,
const unsigned char* info, size_t info_len,
unsigned char* okm, size_t okm_len);
// ECDH shared secret computation (for debugging)
int ecdh_shared_secret(const unsigned char* private_key,
const unsigned char* public_key_x,
unsigned char* shared_secret);
// Base64 encoding function (for debugging)
size_t base64_encode(const unsigned char* data, size_t len, char* output, size_t output_size);
// =============================================================================
// NIP-04 AND NIP-44 ENCRYPTION FUNCTIONS
// =============================================================================
// Note: NOSTR_NIP04_MAX_PLAINTEXT_SIZE already defined in nostr_core.h
#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65536
// NIP-04 encryption (AES-256-CBC)
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
// NIP-04 decryption
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
// NIP-44 encryption (ChaCha20-Poly1305)
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
// NIP-44 encryption with fixed nonce (for testing)
int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
const unsigned char* nonce,
char* output,
size_t output_size);
// NIP-44 decryption
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
// =============================================================================
// BIP39 MNEMONIC FUNCTIONS
// =============================================================================
// Generate mnemonic from entropy
int nostr_bip39_mnemonic_from_bytes(const unsigned char* entropy, size_t entropy_len,
char* mnemonic);
// Validate mnemonic
int nostr_bip39_mnemonic_validate(const char* mnemonic);
// Convert mnemonic to seed
int nostr_bip39_mnemonic_to_seed(const char* mnemonic, const char* passphrase,
unsigned char* seed, size_t seed_len);
// =============================================================================
// BIP32 HD WALLET FUNCTIONS
// =============================================================================
typedef struct {
unsigned char private_key[32];
unsigned char public_key[33];
unsigned char chain_code[32];
uint32_t depth;
uint32_t parent_fingerprint;
uint32_t child_number;
} nostr_hd_key_t;
// Create master key from seed
int nostr_bip32_key_from_seed(const unsigned char* seed, size_t seed_len,
nostr_hd_key_t* master_key);
// Derive child key from parent
int nostr_bip32_derive_child(const nostr_hd_key_t* parent_key, uint32_t child_number,
nostr_hd_key_t* child_key);
// Derive key from path
int nostr_bip32_derive_path(const nostr_hd_key_t* master_key, const uint32_t* path,
size_t path_len, nostr_hd_key_t* derived_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_CRYPTO_H
Binary file not shown.
+54
View File
@@ -0,0 +1,54 @@
#include "nostr_log.h"
#include <stdio.h>
#define NOSTR_LOG_BUFFER_SIZE 4096
static nostr_log_callback_t g_log_callback = NULL;
static void* g_log_user_data = NULL;
static int g_log_min_level = NOSTR_LOG_LEVEL_ERROR;
void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data) {
g_log_callback = cb;
g_log_user_data = user_data;
}
void nostr_set_log_level(nostr_log_level_t min_level) {
if (min_level < NOSTR_LOG_LEVEL_ERROR) {
g_log_min_level = NOSTR_LOG_LEVEL_ERROR;
return;
}
if (min_level > NOSTR_LOG_LEVEL_TRACE) {
g_log_min_level = NOSTR_LOG_LEVEL_TRACE;
return;
}
g_log_min_level = (int)min_level;
}
void nostr_log_vemitf(int level, const char* component, const char* format, va_list args) {
if (!g_log_callback || !format) {
return;
}
if (level < g_log_min_level) {
return;
}
char message[NOSTR_LOG_BUFFER_SIZE];
int written = vsnprintf(message, sizeof(message), format, args);
if (written < 0) {
return;
}
message[sizeof(message) - 1] = '\0';
g_log_callback(level,
component ? component : "unknown",
message,
g_log_user_data);
}
void nostr_log_emitf(int level, const char* component, const char* format, ...) {
va_list args;
va_start(args, format);
nostr_log_vemitf(level, component, format, args);
va_end(args);
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef NOSTR_LOG_H
#define NOSTR_LOG_H
#include <stdarg.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
NOSTR_LOG_LEVEL_ERROR = 1,
NOSTR_LOG_LEVEL_WARN = 2,
NOSTR_LOG_LEVEL_INFO = 3,
NOSTR_LOG_LEVEL_DEBUG = 4,
NOSTR_LOG_LEVEL_TRACE = 5
} nostr_log_level_t;
typedef void (*nostr_log_callback_t)(
int level,
const char* component,
const char* message,
void* user_data
);
void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data);
void nostr_set_log_level(nostr_log_level_t min_level);
void nostr_log_emitf(int level, const char* component, const char* format, ...);
void nostr_log_vemitf(int level, const char* component, const char* format, va_list args);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_LOG_H */
-141
View File
@@ -1,141 +0,0 @@
#ifndef NOSTR_SECP256K1_H
#define NOSTR_SECP256K1_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
/** Opaque data structure that holds a parsed and valid public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_pubkey {
unsigned char data[64];
} nostr_secp256k1_pubkey;
/** Opaque data structure that holds a parsed keypair.
* Guaranteed to be 96 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_keypair {
unsigned char data[96];
} nostr_secp256k1_keypair;
/** Opaque data structure that holds a parsed x-only public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_xonly_pubkey {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
/** Initialize the secp256k1 library. Must be called before any other functions.
* Returns: 1 on success, 0 on failure.
*/
int nostr_secp256k1_context_create(void);
/** Clean up the secp256k1 library resources.
*/
void nostr_secp256k1_context_destroy(void);
/** Verify an elliptic curve secret key.
* Returns: 1: secret key is valid, 0: secret key is invalid
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_ec_seckey_verify(const unsigned char *seckey);
/** Compute the public key for a secret key.
* Returns: 1: secret was valid, public key stored. 0: secret was invalid.
* Out: pubkey: pointer to the created public key.
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey);
/** Create a keypair from a secret key.
* Returns: 1: keypair created, 0: secret key invalid.
* Out: keypair: pointer to the created keypair.
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_keypair_create(nostr_secp256k1_keypair *keypair, const unsigned char *seckey);
/** Get the x-only public key from a keypair.
* Returns: 1 always.
* Out: pubkey: pointer to storage for the x-only public key.
* In: keypair: pointer to a keypair.
*/
int nostr_secp256k1_keypair_xonly_pub(nostr_secp256k1_xonly_pubkey *pubkey, const nostr_secp256k1_keypair *keypair);
/** Parse an x-only public key from bytes.
* Returns: 1: public key parsed, 0: invalid public key.
* Out: pubkey: pointer to the created x-only public key.
* In: input32: pointer to a 32-byte x-only public key.
*/
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey *pubkey, const unsigned char *input32);
/** Serialize an x-only public key to bytes.
* Returns: 1 always.
* Out: output32: pointer to a 32-byte array to store the serialized key.
* In: pubkey: pointer to an x-only public key.
*/
int nostr_secp256k1_xonly_pubkey_serialize(unsigned char *output32, const nostr_secp256k1_xonly_pubkey *pubkey);
/** Create a Schnorr signature.
* Returns: 1: signature created, 0: nonce generation failed or secret key invalid.
* Out: sig64: pointer to a 64-byte array where the signature will be placed.
* In: msghash32: the 32-byte message hash being signed.
* keypair: pointer to an initialized keypair.
* aux_rand32: pointer to 32 bytes of auxiliary randomness (can be NULL).
*/
int nostr_secp256k1_schnorrsig_sign32(unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_keypair *keypair, const unsigned char *aux_rand32);
/** Verify a Schnorr signature.
* Returns: 1: correct signature, 0: incorrect signature
* In: sig64: pointer to the 64-byte signature being verified.
* msghash32: the 32-byte message hash being verified.
* pubkey: pointer to an x-only public key to verify with.
*/
int nostr_secp256k1_schnorrsig_verify(const unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_xonly_pubkey *pubkey);
/** Serialize a pubkey object into a serialized byte sequence.
* Returns: 1 always.
* Out: output: pointer to a 33-byte array to place the serialized key in.
* In: pubkey: pointer to a secp256k1_pubkey containing an initialized public key.
*
* The output will be a 33-byte compressed public key (0x02 or 0x03 prefix + 32 bytes x coordinate).
*/
int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char *output, const nostr_secp256k1_pubkey *pubkey);
/** Tweak a secret key by adding a 32-byte tweak to it.
* Returns: 1: seckey was valid, 0: seckey invalid or resulting key invalid
* In/Out: seckey: pointer to a 32-byte secret key. Will be modified in-place.
* In: tweak: pointer to a 32-byte tweak.
*/
int nostr_secp256k1_ec_seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak);
/** Parse a variable-length public key into the pubkey object.
* Returns: 1: public key parsed, 0: invalid public key.
* Out: pubkey: pointer to the created public key.
* In: input: pointer to a serialized public key
* inputlen: length of the array pointed to by input
*/
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen);
/** Compute an EC Diffie-Hellman secret in constant time.
* Returns: 1: exponentiation was successful, 0: scalar was invalid (zero or overflow)
* Out: result: a 32-byte array which will be populated by an ECDH secret computed from point and scalar
* In: pubkey: a pointer to a secp256k1_pubkey containing an initialized public key
* seckey: a 32-byte scalar with which to multiply the point
*/
int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey, void *hashfp, void *data);
/** Generate cryptographically secure random bytes.
* Returns: 1: success, 0: failure
* Out: buf: buffer to fill with random bytes
* In: len: number of bytes to generate
*/
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_SECP256K1_H */
Binary file not shown.
File diff suppressed because it is too large Load Diff
+274
View File
@@ -0,0 +1,274 @@
/*
* NOSTR Core Library - Request Validator
*
* Unified authentication and authorization system for NOSTR applications.
* Provides rule-based validation for requests with pluggable database backends.
*
* This module combines basic NOSTR event validation with sophisticated
* authentication rules to provide a single entry point for request validation
* across different NOSTR applications (ginxsom, c-relay, etc.).
*/
#ifndef NOSTR_REQUEST_VALIDATOR_H
#define NOSTR_REQUEST_VALIDATOR_H
#include "nostr_common.h"
#include <time.h>
#include <sqlite3.h>
#ifdef __cplusplus
extern "C" {
#endif
// Forward declaration for cJSON
struct cJSON;
// Authentication rule types
typedef enum {
NOSTR_AUTH_RULE_PUBKEY_WHITELIST,
NOSTR_AUTH_RULE_PUBKEY_BLACKLIST,
NOSTR_AUTH_RULE_HASH_BLACKLIST,
NOSTR_AUTH_RULE_MIME_WHITELIST,
NOSTR_AUTH_RULE_MIME_BLACKLIST,
NOSTR_AUTH_RULE_SIZE_LIMIT,
NOSTR_AUTH_RULE_RATE_LIMIT,
NOSTR_AUTH_RULE_CUSTOM
} nostr_auth_rule_type_t;
// Authentication request context
typedef struct {
const char* operation; // Operation type ("upload", "delete", "list", "publish")
const char* auth_header; // Raw authorization header (optional)
struct cJSON* event; // NOSTR event for validation (optional)
// Resource context (for file/blob operations)
const char* resource_hash; // Resource hash (SHA-256, optional)
const char* mime_type; // MIME type (optional)
long file_size; // File size (optional)
// Client context
const char* client_ip; // Client IP for rate limiting (optional)
void* app_context; // Application-specific context (optional)
} nostr_request_t;
// Authentication result
typedef struct {
int valid; // 0 = invalid/denied, 1 = valid/allowed
int error_code; // NOSTR_SUCCESS or specific error code
char reason[256]; // Human-readable reason for denial
char pubkey[65]; // Extracted pubkey from validated event (if available)
int rule_id; // Rule ID that made the decision (0 if no rule)
int priority; // Priority of the rule that matched
time_t cached_until; // Cache expiration time
} nostr_request_result_t;
// Authentication rule definition
typedef struct {
int rule_id; // Unique rule identifier
nostr_auth_rule_type_t type; // Rule type
char operation[32]; // Target operation ("*", "upload", "delete", "publish", etc.)
char target[256]; // Rule target (pubkey, hash, mime pattern, etc.)
char value[256]; // Rule value (size limit, rate limit, custom data)
int priority; // Rule priority (lower number = higher priority)
int enabled; // 1 = enabled, 0 = disabled
time_t expires_at; // Expiration timestamp (0 = never expires)
char description[512]; // Human-readable description
time_t created_at; // Creation timestamp
} nostr_auth_rule_t;
// Database backend interface (pluggable)
typedef struct nostr_auth_db_interface {
const char* name; // Backend name ("sqlite", "redis", etc.)
// Database lifecycle
int (*init)(const char* db_path, const char* app_name);
void (*cleanup)(void);
// Configuration management
int (*get_config)(const char* key, char* value, size_t value_size);
int (*set_config)(const char* key, const char* value);
// Rule querying and management
int (*query_rules)(const nostr_request_t* request, nostr_auth_rule_t** rules, int* count);
int (*rule_add)(const nostr_auth_rule_t* rule);
int (*rule_remove)(int rule_id);
int (*rule_update)(const nostr_auth_rule_t* rule);
int (*rule_list)(const char* operation, nostr_auth_rule_t** rules, int* count);
// Caching operations
int (*cache_get)(const char* cache_key, nostr_request_result_t* result);
int (*cache_set)(const char* cache_key, const nostr_request_result_t* result, int ttl);
int (*cache_clear)(void);
} nostr_auth_db_interface_t;
//=============================================================================
// CORE API FUNCTIONS
//=============================================================================
/**
* Initialize the request validator system with application database
*
* @param app_db_path Path to application's SQLite database
* @param app_name Application name for logging/identification
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_request_validator_init(const char* app_db_path, const char* app_name);
/**
* Initialize with shared database (future use)
*
* @param shared_db_path Path to shared authentication database
* @param app_name Application name for logging/identification
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_request_validator_init_shared(const char* shared_db_path, const char* app_name);
/**
* Main request validation function - validates both NOSTR events and authentication rules
*
* @param request Request context with operation, auth header, and resource details
* @param result Result structure with validation outcome and details
* @return NOSTR_SUCCESS on successful validation processing, error code on system failure
*/
int nostr_validate_request(const nostr_request_t* request, nostr_request_result_t* result);
/**
* Check if authentication rules system is enabled
*
* @return 1 if enabled, 0 if disabled
*/
int nostr_auth_rules_enabled(void);
/**
* Cleanup request validator resources
*/
void nostr_request_validator_cleanup(void);
//=============================================================================
// CONVENIENCE FUNCTIONS
//=============================================================================
/**
* Convenience function for upload validation (ginxsom integration)
*
* @param pubkey Uploader public key (optional, extracted from auth if NULL)
* @param auth_header Authorization header with NOSTR event
* @param hash File hash (SHA-256)
* @param mime_type File MIME type
* @param file_size File size in bytes
* @return NOSTR_SUCCESS if allowed, error code if denied
*/
int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
const char* hash, const char* mime_type, long file_size);
/**
* Convenience function for delete validation (ginxsom integration)
*
* @param pubkey Requester public key
* @param auth_header Authorization header with NOSTR event
* @param hash File hash to delete
* @return NOSTR_SUCCESS if allowed, error code if denied
*/
int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const char* hash);
/**
* Convenience function for publish validation (c-relay integration)
*
* @param pubkey Publisher public key
* @param event NOSTR event to publish
* @return NOSTR_SUCCESS if allowed, error code if denied
*/
int nostr_auth_check_publish(const char* pubkey, struct cJSON* event);
//=============================================================================
// RULE MANAGEMENT FUNCTIONS
//=============================================================================
/**
* Add a new authentication rule
*
* @param rule Rule definition to add
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_add(const nostr_auth_rule_t* rule);
/**
* Remove an authentication rule by ID
*
* @param rule_id Rule ID to remove
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_remove(int rule_id);
/**
* Update an existing authentication rule
*
* @param rule Updated rule definition
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_update(const nostr_auth_rule_t* rule);
/**
* List authentication rules for a specific operation
*
* @param operation Target operation ("*" for all operations)
* @param rules Pointer to receive allocated array of rules
* @param count Pointer to receive number of rules returned
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int* count);
/**
* Free rule array allocated by nostr_auth_rule_list
*
* @param rules Rule array to free
* @param count Number of rules in array
*/
void nostr_auth_rules_free(nostr_auth_rule_t* rules, int count);
//=============================================================================
// DATABASE BACKEND MANAGEMENT
//=============================================================================
/**
* Register a database backend implementation
*
* @param backend Backend interface implementation
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_register_db_backend(const nostr_auth_db_interface_t* backend);
/**
* Set active database backend by name
*
* @param backend_name Name of backend to activate
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_set_db_backend(const char* backend_name);
//=============================================================================
// CACHE MANAGEMENT
//=============================================================================
/**
* Clear authentication decision cache
*
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_cache_clear(void);
/**
* Get cache statistics
*
* @param hit_count Pointer to receive cache hit count
* @param miss_count Pointer to receive cache miss count
* @param entries Pointer to receive current number of cache entries
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_cache_stats(int* hit_count, int* miss_count, int* entries);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_REQUEST_VALIDATOR_H */
File diff suppressed because it is too large Load Diff
+189
View File
@@ -0,0 +1,189 @@
/*
* NOSTR Core Library - Utilities
*
* General utility functions used across multiple NIPs
*/
#ifndef NOSTR_UTILS_H
#define NOSTR_UTILS_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
// Convert bytes to hexadecimal string
void nostr_bytes_to_hex(const unsigned char *bytes, size_t len, char *hex);
// Convert hexadecimal string to bytes
int nostr_hex_to_bytes(const char *hex, unsigned char *bytes, size_t len);
// Base64 encoding function
size_t base64_encode(const unsigned char *data, size_t len, char *output,
size_t output_size);
// Base64 decoding function
size_t base64_decode(const char *input, unsigned char *output);
// =============================================================================
// CORE CRYPTO FUNCTIONS
// =============================================================================
// Initialize crypto subsystem
int nostr_crypto_init(void);
// Cleanup crypto subsystem
void nostr_crypto_cleanup(void);
// SHA-256 hash function
int nostr_sha256(const unsigned char *data, size_t len, unsigned char *hash);
// =============================================================================
// STREAMING SHA-256 FUNCTIONS
// =============================================================================
// SHA-256 streaming context
typedef struct {
uint32_t state[8]; // Current hash state
unsigned char buffer[64]; // Input buffer for incomplete blocks
uint64_t bitlen; // Total bits processed
size_t buflen; // Current buffer length
} nostr_sha256_ctx_t;
// Initialize SHA-256 streaming context
int nostr_sha256_init(nostr_sha256_ctx_t* ctx);
// Update SHA-256 context with new data
int nostr_sha256_update(nostr_sha256_ctx_t* ctx, const unsigned char* data, size_t len);
// Finalize SHA-256 and output hash
int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash);
// Stream SHA-256 hash of a file
int nostr_sha256_file_stream(const char* filename, unsigned char* hash);
// HMAC-SHA256
int nostr_hmac_sha256(const unsigned char *key, size_t key_len,
const unsigned char *data, size_t data_len,
unsigned char *output);
// HMAC-SHA512
int nostr_hmac_sha512(const unsigned char *key, size_t key_len,
const unsigned char *data, size_t data_len,
unsigned char *output);
// PBKDF2 with HMAC-SHA512
int nostr_pbkdf2_hmac_sha512(const unsigned char *password, size_t password_len,
const unsigned char *salt, size_t salt_len,
int iterations, unsigned char *output,
size_t output_len);
// SHA-512 implementation (for testing)
int nostr_sha512(const unsigned char *data, size_t len, unsigned char *hash);
// =============================================================================
// SECP256K1 ELLIPTIC CURVE FUNCTIONS
// =============================================================================
// Verify private key is valid
int nostr_ec_private_key_verify(const unsigned char *private_key);
// Generate public key from private key
int nostr_ec_public_key_from_private_key(const unsigned char *private_key,
unsigned char *public_key);
// Sign data with ECDSA
int nostr_ec_sign(const unsigned char *private_key, const unsigned char *hash,
unsigned char *signature);
// RFC 6979 deterministic nonce generation
int nostr_rfc6979_generate_k(const unsigned char *private_key,
const unsigned char *message_hash,
unsigned char *k_out);
int nostr_schnorr_sign(const unsigned char* private_key,
const unsigned char* hash,
unsigned char* signature);
// =============================================================================
// HKDF KEY DERIVATION FUNCTIONS
// =============================================================================
// HKDF Extract step
int nostr_hkdf_extract(const unsigned char *salt, size_t salt_len,
const unsigned char *ikm, size_t ikm_len,
unsigned char *prk);
// HKDF Expand step
int nostr_hkdf_expand(const unsigned char *prk, size_t prk_len,
const unsigned char *info, size_t info_len,
unsigned char *okm, size_t okm_len);
// HKDF (Extract + Expand)
int nostr_hkdf(const unsigned char *salt, size_t salt_len,
const unsigned char *ikm, size_t ikm_len,
const unsigned char *info, size_t info_len, unsigned char *okm,
size_t okm_len);
// ECDH shared secret computation (for debugging)
int ecdh_shared_secret(const unsigned char *private_key,
const unsigned char *public_key_x,
unsigned char *shared_secret);
// =============================================================================
// BIP39 MNEMONIC FUNCTIONS
// =============================================================================
// Generate mnemonic from entropy
int nostr_bip39_mnemonic_from_bytes(const unsigned char *entropy,
size_t entropy_len, char *mnemonic);
// Validate mnemonic
int nostr_bip39_mnemonic_validate(const char *mnemonic);
// Convert mnemonic to seed
int nostr_bip39_mnemonic_to_seed(const char *mnemonic, const char *passphrase,
unsigned char *seed, size_t seed_len);
// =============================================================================
// BIP32 HD WALLET FUNCTIONS
// =============================================================================
typedef struct {
unsigned char private_key[32];
unsigned char public_key[33];
unsigned char chain_code[32];
uint32_t depth;
uint32_t parent_fingerprint;
uint32_t child_number;
} nostr_hd_key_t;
// Create master key from seed
int nostr_bip32_key_from_seed(const unsigned char *seed, size_t seed_len,
nostr_hd_key_t *master_key);
// Derive child key from parent
int nostr_bip32_derive_child(const nostr_hd_key_t *parent_key,
uint32_t child_number, nostr_hd_key_t *child_key);
// Derive key from path
int nostr_bip32_derive_path(const nostr_hd_key_t *master_key,
const uint32_t *path, size_t path_len,
nostr_hd_key_t *derived_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_UTILS_H
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"git.ignoreLimitWarning": true
}
}
+33 -12
View File
@@ -8,11 +8,11 @@ The NOSTR WebSocket library consists of these key files for export:
### Core Files (Required)
- `nostr_websocket_tls.h` - Header with all function declarations and constants
- `nostr_websocket_mbedtls.c` - Main implementation using mbedTLS for SSL/TLS
- `nostr_websocket_openssl.c` - Main implementation using OpenSSL for SSL/TLS
- `../cjson/cJSON.h` and `../cjson/cJSON.c` - JSON parsing (lightweight)
### Dependencies
- **mbedTLS** - For SSL/TLS support (wss:// connections)
- **OpenSSL** - For SSL/TLS support (wss:// connections) - libssl, libcrypto
- **Standard C libraries** - socket, networking, etc.
## Quick Integration
@@ -21,23 +21,29 @@ The NOSTR WebSocket library consists of these key files for export:
```bash
# Copy the library files
cp nostr_websocket_tls.h your_project/
cp nostr_websocket_mbedtls.c your_project/
cp nostr_websocket_openssl.c your_project/
cp ../cjson/cJSON.h your_project/
cp ../cjson/cJSON.c your_project/
```
### 2. Install mbedTLS Dependency
### 2. Install OpenSSL Dependency
```bash
# Ubuntu/Debian
sudo apt-get install libmbedtls-dev
sudo apt-get install libssl-dev
# Or build from source (see mbedTLS documentation)
# CentOS/RHEL/Fedora
sudo yum install openssl-devel
# or
sudo dnf install openssl-devel
# macOS (via Homebrew)
brew install openssl
```
### 3. Compile Your Project
```bash
gcc -o my_nostr_app my_app.c nostr_websocket_mbedtls.c cJSON.c \
-lmbedtls -lmbedx509 -lmbedcrypto -lm
gcc -o my_nostr_app my_app.c nostr_websocket_openssl.c cJSON.c \
-lssl -lcrypto -lm
```
## Basic Usage Example
@@ -131,16 +137,17 @@ The library supports both TCP (`ws://`) and TLS (`wss://`) automatically based o
### Linux/Unix
- Works out of the box with standard development tools
- Requires: gcc, mbedTLS development headers
- Requires: gcc, OpenSSL development headers (libssl-dev)
### Potential Windows Support
### Windows Support
- Would need Winsock2 adaptations in transport layer
- mbedTLS is cross-platform compatible
- OpenSSL is widely available on Windows
### Embedded Systems
- Lightweight design suitable for embedded use
- Memory usage: ~4KB per client + message buffers
- No dynamic allocations in hot paths
- OpenSSL provides optimized implementations for various architectures
## Library Design Benefits
@@ -178,7 +185,21 @@ The library handles all WebSocket protocol details, SSL/TLS, and NOSTR message f
- Based on WebSocket RFC 6455
- Implements NOSTR WebSocket conventions
- SSL/TLS via proven mbedTLS library
- SSL/TLS via industry-standard OpenSSL library
- Tested with major NOSTR relays
## Migration Notes
If you were previously using the mbedTLS version of this library:
### Code Changes Required
- Update `#include` to reference `nostr_websocket_openssl.c` instead of `nostr_websocket_mbedtls.c`
- Change linking flags from `-lmbedtls -lmbedx509 -lmbedcrypto` to `-lssl -lcrypto`
- Install OpenSSL development headers instead of mbedTLS
### API Compatibility
- All function signatures remain identical
- No changes to your application code required
- Same performance characteristics and behavior
This library provides a clean, efficient way to integrate NOSTR WebSocket functionality into any C project with minimal dependencies and maximum portability.
-63
View File
@@ -1,63 +0,0 @@
# NOSTR WebSocket Library Makefile
# Production-ready WebSocket implementation for NOSTR protocol
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -O2
INCLUDES = -I. -I.. -I../mbedtls/include -I../mbedtls/tf-psa-crypto/include -I../mbedtls/tf-psa-crypto/drivers/builtin/include
LIBS = -lm -L../mbedtls/library -lmbedtls -lmbedx509 -lmbedcrypto
# Source files
WEBSOCKET_SOURCES = nostr_websocket_mbedtls.c ../cjson/cJSON.c
WEBSOCKET_HEADERS = nostr_websocket_tls.h ../cjson/cJSON.h
# Test programs
TEST_SOURCES = test_5_events_clean.c
TEST_PROGRAMS = test_5_events_clean
# Object files
WEBSOCKET_OBJECTS = $(WEBSOCKET_SOURCES:.c=.o)
.PHONY: all clean test
all: $(TEST_PROGRAMS)
# Test programs
test_5_events_clean: test_5_events_clean.o $(WEBSOCKET_OBJECTS)
$(CC) -o $@ $^ $(LIBS)
# Object file compilation
%.o: %.c $(WEBSOCKET_HEADERS)
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
# Test target
test: test_5_events_clean
@echo "🧪 Running WebSocket test..."
@echo "Press Ctrl-C to stop the test"
./test_5_events_clean
# Clean build artifacts
clean:
rm -f *.o $(TEST_PROGRAMS)
rm -f ../cjson/*.o
# Display library info
info:
@echo "📚 NOSTR WebSocket Library"
@echo "=========================="
@echo "Core files:"
@echo " - nostr_websocket_tls.h (header)"
@echo " - nostr_websocket_mbedtls.c (implementation)"
@echo " - ../cjson/cJSON.h/c (JSON support)"
@echo ""
@echo "Dependencies:"
@echo " - mbedTLS (SSL/TLS support)"
@echo " - Standard C libraries"
@echo ""
@echo "Usage:"
@echo " make - Build test programs"
@echo " make test - Run WebSocket test"
@echo " make clean - Clean build artifacts"
@echo " make info - Show this information"
# Help target
help: info
+6 -4
View File
@@ -5,7 +5,7 @@ A production-ready, lightweight WebSocket client library specifically designed f
## Features
- ✅ **WebSocket RFC 6455 Compliant** - Full WebSocket protocol implementation
- ✅ **SSL/TLS Support** - Secure `wss://` connections via mbedTLS
- ✅ **SSL/TLS Support** - Secure `wss://` connections via OpenSSL
- ✅ **NOSTR Protocol** - Built-in support for REQ, EVENT, CLOSE messages
- ✅ **Production Ready** - Optimized performance and error handling
- ✅ **Lightweight** - Minimal dependencies and memory footprint
@@ -54,11 +54,11 @@ cJSON_Delete(filter);
### Core Components
- **`nostr_websocket_tls.h`** - Public API header
- **`nostr_websocket_mbedtls.c`** - Main implementation (mbedTLS backend)
- **`nostr_websocket_openssl.c`** - Main implementation (OpenSSL backend)
- **`../cjson/cJSON.h/c`** - JSON parsing support
### Dependencies
- **mbedTLS** - For SSL/TLS support
- **OpenSSL** - For SSL/TLS support (libssl, libcrypto)
- **Standard C libraries** - POSIX sockets, etc.
## Installation in Other Projects
@@ -121,14 +121,16 @@ make help # Show help
## Development History
This library evolved from the experimental WebSocket implementation in `../websocket_experiment/` and represents the production-ready, stable version suitable for integration into other projects.
This library evolved from the experimental WebSocket implementation and represents the production-ready, stable version suitable for integration into other projects.
Key improvements made during development:
- **Migrated from mbedTLS to OpenSSL** - Better compatibility and performance
- Fixed critical WebSocket frame parsing bugs
- Optimized SSL/TLS performance
- Reduced memory allocations
- Enhanced error handling
- Added comprehensive documentation
- Improved build system integration
## License
@@ -1,5 +1,6 @@
#define _GNU_SOURCE
#include "nostr_websocket_tls.h"
#include "../nostr_core/nostr_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -12,14 +13,11 @@
#include <time.h>
#include <sys/select.h>
// mbedTLS headers
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/error.h"
#include "mbedtls/debug.h"
#include "psa/crypto.h"
// OpenSSL headers
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/x509v3.h>
// WebSocket magic string for handshake
#define WS_MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
@@ -27,14 +25,6 @@
#define MAX_HEADER_SIZE 4096
#define MAX_FRAME_SIZE 65536
// Debug logging (conditional compilation)
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
static FILE* debug_log_file = NULL;
static void debug_log_init(void);
static void debug_log_message(const char* direction, const char* host, int port, const char* message);
static const char* get_timestamp(void);
#endif
// Transport layer abstraction
typedef struct {
int (*connect)(void* ctx, const char* host, int port);
@@ -49,13 +39,11 @@ typedef struct {
int socket_fd;
} tcp_transport_t;
// TLS transport context (mbedTLS)
// TLS transport context (OpenSSL)
typedef struct {
int socket_fd;
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
SSL_CTX* ssl_ctx;
SSL* ssl;
int ssl_connected;
} tls_transport_t;
@@ -117,12 +105,28 @@ static transport_ops_t tls_transport_ops = {
.cleanup = tls_cleanup
};
// Global OpenSSL initialization flag
static int openssl_initialized = 0;
// Initialize OpenSSL
static void init_openssl(void) {
if (!openssl_initialized) {
SSL_load_error_strings();
SSL_library_init();
OpenSSL_add_all_algorithms();
openssl_initialized = 1;
}
}
// ============================================================================
// Core WebSocket Functions
// ============================================================================
nostr_ws_client_t* nostr_ws_connect(const char* url) {
if (!url) return NULL;
// Initialize OpenSSL
init_openssl();
nostr_ws_client_t* client = calloc(1, sizeof(nostr_ws_client_t));
if (!client) return NULL;
@@ -442,6 +446,13 @@ static int tcp_connect(void* ctx, const char* host, int port) {
tcp->socket_fd = -1;
return -1;
}
// Safety timeout: prevent indefinite blocking in recv/send on half-broken sockets.
struct timeval io_timeout;
io_timeout.tv_sec = 5;
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));
return 0;
}
@@ -485,45 +496,9 @@ static void tcp_cleanup(void* ctx) {
}
// ============================================================================
// TLS Transport Implementation (mbedTLS)
// TLS Transport Implementation (OpenSSL)
// ============================================================================
// Custom certificate verification callback (accept all certificates for NOSTR)
static int verify_cert(void *data, mbedtls_x509_crt *crt, int depth, uint32_t *flags) {
(void)data; (void)crt; (void)depth;
*flags = 0; // Clear all verification flags (accept any certificate)
return 0;
}
// Custom send function for mbedTLS
static int mbedtls_send(void *ctx, const unsigned char *buf, size_t len) {
int sock_fd = *((int*)ctx);
ssize_t sent = send(sock_fd, buf, len, MSG_NOSIGNAL);
if (sent < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return MBEDTLS_ERR_SSL_WANT_WRITE;
}
return MBEDTLS_ERR_NET_SEND_FAILED;
}
return sent;
}
// Custom receive function for mbedTLS
static int mbedtls_recv(void *ctx, unsigned char *buf, size_t len) {
int sock_fd = *((int*)ctx);
ssize_t received = recv(sock_fd, buf, len, 0);
if (received < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return MBEDTLS_ERR_SSL_WANT_READ;
}
return MBEDTLS_ERR_NET_RECV_FAILED;
}
if (received == 0) {
return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY;
}
return received;
}
static int tls_connect(void* ctx, const char* host, int port) {
tls_transport_t* tls = (tls_transport_t*)ctx;
int ret;
@@ -535,68 +510,50 @@ static int tls_connect(void* ctx, const char* host, int port) {
}
tls->socket_fd = tcp_ctx.socket_fd;
// Initialize PSA crypto (required for this version of mbedTLS)
psa_status_t psa_status = psa_crypto_init();
if (psa_status != PSA_SUCCESS) {
// Create SSL context
tls->ssl_ctx = SSL_CTX_new(TLS_client_method());
if (!tls->ssl_ctx) {
goto cleanup;
}
// Initialize mbedTLS structures
mbedtls_ssl_init(&tls->ssl);
mbedtls_ssl_config_init(&tls->conf);
mbedtls_entropy_init(&tls->entropy);
mbedtls_ctr_drbg_init(&tls->ctr_drbg);
// Seed the random number generator
const char *pers = "nostr_ws_client";
if ((ret = mbedtls_ctr_drbg_seed(&tls->ctr_drbg, mbedtls_entropy_func, &tls->entropy,
(const unsigned char *)pers, strlen(pers))) != 0) {
goto cleanup;
}
// Set up SSL configuration
if ((ret = mbedtls_ssl_config_defaults(&tls->conf,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
goto cleanup;
}
// RNG is configured automatically through mbedtls_ssl_config_defaults
// The CTR_DRBG we initialized should be used automatically
// Set up SSL context options
SSL_CTX_set_options(tls->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
// Disable certificate verification for NOSTR (we only need encryption)
mbedtls_ssl_conf_authmode(&tls->conf, MBEDTLS_SSL_VERIFY_NONE);
mbedtls_ssl_conf_verify(&tls->conf, verify_cert, NULL);
SSL_CTX_set_verify(tls->ssl_ctx, SSL_VERIFY_NONE, NULL);
// Set up SSL context
if ((ret = mbedtls_ssl_setup(&tls->ssl, &tls->conf)) != 0) {
// Create SSL connection
tls->ssl = SSL_new(tls->ssl_ctx);
if (!tls->ssl) {
goto cleanup;
}
// Set hostname for SNI
if ((ret = mbedtls_ssl_set_hostname(&tls->ssl, host)) != 0) {
SSL_set_tlsext_host_name(tls->ssl, host);
// Associate socket with SSL connection
if (SSL_set_fd(tls->ssl, tls->socket_fd) != 1) {
goto cleanup;
}
// Set I/O functions
mbedtls_ssl_set_bio(&tls->ssl, &tls->socket_fd, mbedtls_send, mbedtls_recv, NULL);
// Perform TLS handshake
while ((ret = mbedtls_ssl_handshake(&tls->ssl)) != 0) {
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
goto cleanup;
}
ret = SSL_connect(tls->ssl);
if (ret <= 0) {
goto cleanup;
}
tls->ssl_connected = 1;
return 0;
cleanup:
mbedtls_ssl_free(&tls->ssl);
mbedtls_ssl_config_free(&tls->conf);
mbedtls_ctr_drbg_free(&tls->ctr_drbg);
mbedtls_entropy_free(&tls->entropy);
if (tls->ssl) {
SSL_free(tls->ssl);
tls->ssl = NULL;
}
if (tls->ssl_ctx) {
SSL_CTX_free(tls->ssl_ctx);
tls->ssl_ctx = NULL;
}
close(tls->socket_fd);
tls->socket_fd = -1;
return -1;
@@ -604,11 +561,12 @@ cleanup:
static int tls_send(void* ctx, const void* data, size_t len) {
tls_transport_t* tls = (tls_transport_t*)ctx;
if (!tls->ssl_connected) return -1;
if (!tls->ssl_connected || !tls->ssl) return -1;
int ret = mbedtls_ssl_write(&tls->ssl, (const unsigned char*)data, len);
if (ret < 0) {
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
int ret = SSL_write(tls->ssl, data, len);
if (ret <= 0) {
int err = SSL_get_error(tls->ssl, ret);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
return -1; // Would block
}
return -1;
@@ -618,28 +576,34 @@ static int tls_send(void* ctx, const void* data, size_t len) {
static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
tls_transport_t* tls = (tls_transport_t*)ctx;
if (!tls->ssl_connected) {
if (!tls->ssl_connected || !tls->ssl) {
return -1;
}
// Check if mbedTLS has pending data first - this avoids unnecessary select() calls
size_t pending = mbedtls_ssl_get_bytes_avail(&tls->ssl);
if (pending == 0 && timeout_ms > 0) {
// Only use select() if no data is pending in SSL buffers
// Always use select() to ensure proper blocking behavior
// If SSL has pending data, use zero timeout to return immediately
// Otherwise use the full timeout to block until data arrives
if (timeout_ms > 0) {
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(tls->socket_fd, &readfds);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
// If SSL has buffered data, use zero timeout; otherwise use full timeout
if (SSL_pending(tls->ssl) > 0) {
tv.tv_sec = 0;
tv.tv_usec = 0;
} else {
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
}
int result = select(tls->socket_fd + 1, &readfds, NULL, NULL, &tv);
if (result < 0) {
return -1;
} else if (result == 0) {
return -1; // Timeout
} else if (result == 0 && SSL_pending(tls->ssl) == 0) {
return -1; // Timeout with no pending data
}
}
@@ -648,13 +612,15 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
const int max_attempts = 10;
while (attempts < max_attempts) {
int ret = mbedtls_ssl_read(&tls->ssl, (unsigned char*)data, len);
int ret = SSL_read(tls->ssl, data, len);
if (ret >= 0) {
return ret; // Success or clean close
if (ret > 0) {
return ret; // Success
}
if (ret == MBEDTLS_ERR_SSL_WANT_READ) {
int err = SSL_get_error(tls->ssl, ret);
if (err == SSL_ERROR_WANT_READ) {
// Check if more data is available on socket
fd_set readfds;
struct timeval tv = {0, 100000}; // 100ms timeout
@@ -670,16 +636,12 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
attempts++;
continue; // Retry the SSL read
} else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
} else if (err == SSL_ERROR_WANT_WRITE) {
return -1;
} else if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
} else if (err == SSL_ERROR_ZERO_RETURN) {
return 0; // Clean close
} else if (ret == -0x7b00) { // MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET
attempts++;
continue; // This is normal, keep trying to read application data
} else {
return -1;
}
@@ -690,8 +652,8 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
static int tls_close(void* ctx) {
tls_transport_t* tls = (tls_transport_t*)ctx;
if (tls->ssl_connected) {
mbedtls_ssl_close_notify(&tls->ssl);
if (tls->ssl_connected && tls->ssl) {
SSL_shutdown(tls->ssl);
tls->ssl_connected = 0;
}
@@ -708,10 +670,15 @@ static void tls_cleanup(void* ctx) {
tls_close(ctx);
mbedtls_ssl_free(&tls->ssl);
mbedtls_ssl_config_free(&tls->conf);
mbedtls_ctr_drbg_free(&tls->ctr_drbg);
mbedtls_entropy_free(&tls->entropy);
if (tls->ssl) {
SSL_free(tls->ssl);
tls->ssl = NULL;
}
if (tls->ssl_ctx) {
SSL_CTX_free(tls->ssl_ctx);
tls->ssl_ctx = NULL;
}
}
// ============================================================================
@@ -765,10 +732,13 @@ static int ws_parse_url(const char* url, char** host, int* port, char** path, in
static int ws_create_handshake_key(char* key_out, size_t key_size) {
if (key_size < WS_KEY_LEN + 1) return -1;
// Generate random 16 bytes
// Generate random 16 bytes using OpenSSL
unsigned char random_bytes[16];
for (int i = 0; i < 16; i++) {
random_bytes[i] = rand() & 0xFF;
if (RAND_bytes(random_bytes, 16) != 1) {
// Fallback to basic rand if OpenSSL fails
for (int i = 0; i < 16; i++) {
random_bytes[i] = rand() & 0xFF;
}
}
// Base64 encode
@@ -818,32 +788,35 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
// Read response
char response[MAX_HEADER_SIZE];
int total_received = 0;
while ((size_t)total_received < sizeof(response) - 1) {
int received = client->transport->recv(&client->transport_ctx,
response + total_received,
sizeof(response) - total_received - 1,
client->timeout_ms);
int received = client->transport->recv(&client->transport_ctx,
response + total_received,
sizeof(response) - total_received - 1,
client->timeout_ms);
if (received <= 0) {
return -1;
}
total_received += received;
response[total_received] = '\0';
// Check if we have complete headers
if (strstr(response, "\r\n\r\n")) break;
if (strstr(response, "\r\n\r\n")) {
break;
}
}
// Check if response starts with the correct HTTP status line
if (strncmp(response, "HTTP/1.1 101 Switching Protocols\r\n", 34) != 0) {
return -1;
}
if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket")) {
if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket") &&
!strstr(response, "Upgrade: WebSocket") && !strstr(response, "upgrade: WebSocket")) {
return -1;
}
return 0;
}
@@ -887,10 +860,15 @@ static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const ch
frame_len += payload_len;
}
// Log outgoing message to debug.log
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
// Emit outgoing message to consumer callback logger
#if defined(ENABLE_WEBSOCKET_LOGGING)
if (opcode == WS_OPCODE_TEXT && payload && payload_len > 0) {
debug_log_message("SEND", client->host, client->port, payload);
nostr_log_emitf(NOSTR_LOG_LEVEL_TRACE,
"websocket",
"SEND %s:%d: %s",
client->host ? client->host : "",
client->port,
payload);
}
#endif
@@ -935,11 +913,6 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
header_len = 10;
}
// Check payload length
if (len > *payload_len) {
return -1;
}
// Read mask (if present)
uint32_t mask = 0;
if (masked) {
@@ -952,6 +925,21 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
header[header_len + 3];
header_len += 4;
}
// Check payload length; if too large for caller buffer, drain frame to keep stream aligned
if (len > *payload_len) {
char discard[1024];
uint64_t remaining = len;
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);
if (got <= 0) {
return -1;
}
remaining -= (uint64_t)got;
}
return -1;
}
// Read payload
if (len > 0) {
@@ -970,14 +958,19 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
ws_mask_payload(payload, len, mask);
}
// Log incoming text messages to debug.log
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
// Emit incoming text messages to consumer callback logger
#if defined(ENABLE_WEBSOCKET_LOGGING)
if (*opcode == WS_OPCODE_TEXT && len > 0) {
// Null terminate for logging
char temp_payload[len + 1];
memcpy(temp_payload, payload, len);
temp_payload[len] = '\0';
debug_log_message("RECV", client->host, client->port, temp_payload);
nostr_log_emitf(NOSTR_LOG_LEVEL_TRACE,
"websocket",
"RECV %s:%d: %s",
client->host ? client->host : "",
client->port,
temp_payload);
}
#endif
}
@@ -1000,47 +993,11 @@ static void ws_mask_payload(char* payload, size_t len, uint32_t mask) {
}
static uint32_t ws_generate_mask(void) {
uint32_t mask;
if (RAND_bytes((unsigned char*)&mask, sizeof(mask)) == 1) {
return mask;
}
// Fallback to basic rand if OpenSSL fails
return ((uint32_t)rand() << 16) | ((uint32_t)rand() & 0xFFFF);
}
// ============================================================================
// Debug Logging Functions
// ============================================================================
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
static void debug_log_init(void) {
if (!debug_log_file) {
debug_log_file = fopen("debug.log", "a");
if (debug_log_file) {
fprintf(debug_log_file, "\n=== NOSTR WebSocket Debug Log Started ===\n");
fflush(debug_log_file);
}
}
}
static const char* get_timestamp(void) {
static char timestamp[32];
struct timespec ts;
struct tm *timeinfo;
clock_gettime(CLOCK_REALTIME, &ts);
timeinfo = localtime(&ts.tv_sec);
// Format: HH:MM:SS.mmm (with milliseconds)
strftime(timestamp, sizeof(timestamp), "%H:%M:%S", timeinfo);
snprintf(timestamp + 8, sizeof(timestamp) - 8, ".%03ld", ts.tv_nsec / 1000000);
return timestamp;
}
static void debug_log_message(const char* direction, const char* host, int port, const char* message) {
debug_log_init();
if (debug_log_file) {
fprintf(debug_log_file, "[%s] %s %s:%d: %s\n",
get_timestamp(), direction, host, port, message);
fflush(debug_log_file);
}
}
#endif
+1 -1
View File
@@ -13,7 +13,7 @@
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"type": "module",
"dependencies": {
"nostr-tools": "^2.16.1"
}
+148
View File
@@ -0,0 +1,148 @@
# Logging Delegation Plan for `nostr_core_lib`
## Objective
Refactor `nostr_core_lib` so it does not own log destinations such as `debug.log`, and instead emits logs through a consumer-provided callback. This allows host applications such as [`didactyl`](../README.md) to route all library logs into their own logging system with consistent formatting, level control, and file policy.
## Current State and Problem
The library currently writes directly to `debug.log` in multiple places:
- [`nostr_core_lib/nostr_websocket/nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1008)
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:178)
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:242)
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:270)
This creates collisions with consumer logs when the host app also uses `debug.log` such as [`didactyl/src/debug.c`](../../src/debug.c:18).
## Target Design
Adopt a callback-based logging API inside `nostr_core_lib`:
1. Library owns no file handle for logs.
2. Library emits structured log events by level and source.
3. Consumer registers a callback once during startup.
4. If no callback is set, logs are dropped by default.
### High-Level Flow
```mermaid
flowchart TD
A[nostr_core_lib internal code] --> B[nostr_log_emit level source message]
B --> C{callback configured}
C -->|Yes| D[consumer callback]
C -->|No| E[drop log event]
D --> F[didactyl debug_log routing]
F --> G[didactyl log file or stdout]
```
## API Additions
Add a small public logging API in [`nostr_core_lib/nostr_core/nostr_core.h`](../nostr_core/nostr_core.h):
- `nostr_log_level_t` enum with levels compatible with host systems
- `nostr_log_callback_t` callback type
- `void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data);`
- `void nostr_set_log_level(nostr_log_level_t min_level);`
Recommended callback shape:
```c
typedef void (*nostr_log_callback_t)(
int level,
const char* component,
const char* message,
void* user_data
);
```
Notes:
- Keep ABI simple by formatting message inside the library before callback invocation.
- Include `component` values such as `websocket` and `nip013`.
## Internal Implementation Plan
## Phase 1: Logging Core
1. Create `nostr_core_lib/nostr_core/nostr_log.h` and `nostr_core_lib/nostr_core/nostr_log.c`.
2. Add internal singleton state:
- callback pointer
- callback user data
- minimum log level
3. Add internal helpers:
- `nostr_log_emitf level component fmt ...`
- stack buffer formatting with truncation safety
4. Default behavior when no callback set: no-op.
## Phase 2: Replace File Logging in WebSocket Layer
1. Remove file-specific static globals from [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:27).
2. Remove `debug_log_init` and `debug_log_message` path at [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1005).
3. Replace call sites:
- outgoing frame logging near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:871)
- incoming frame logging near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:964)
4. Emit through `nostr_log_emitf` with component `websocket`.
## Phase 3: Replace File Logging in NIP-13
1. Replace `fopen`/`fprintf` blocks in [`nip013.c`](../nostr_core/nip013.c:178), [`nip013.c`](../nostr_core/nip013.c:242), and [`nip013.c`](../nostr_core/nip013.c:270).
2. Emit equivalent debug lines via `nostr_log_emitf` with component `nip013`.
3. Preserve existing compile-time verbosity guards if needed, but route output through callback.
## Phase 4: Wire Public API
1. Export new functions in [`nostr_core.h`](../nostr_core/nostr_core.h:1).
2. Ensure object file inclusion in `nostr_core_lib` build scripts and static archive.
3. Document behavior in [`nostr_core_lib/README.md`](../README.md).
## Phase 5: Consumer Integration in Didactyl
1. In didactyl initialization path near [`src/nostr_handler.c`](../../src/nostr_handler.c:1519), register `nostr_set_log_callback`.
2. Adapter callback maps library levels to didactyl levels from [`src/debug.h`](../../src/debug.h:7).
3. Prefix messages with source namespace such as `[nostr:websocket]` and `[nostr:nip013]`.
## Backward Compatibility Strategy
- Keep existing compile guards but change behavior from file output to callback emission.
- If callback not set, behavior remains safe and silent.
- No breaking changes for consumers not using logging.
## Testing Plan
### Unit Tests in `nostr_core_lib`
1. Add tests for callback registration and null behavior.
2. Add tests for level filtering.
3. Add tests that message formatting truncates safely and remains null terminated.
### Integration Checks in Didactyl
1. Start didactyl with debug enabled and callback wired.
2. Verify websocket SEND and RECV lines appear through didactyl logger format from [`src/debug.c`](../../src/debug.c:31).
3. Verify no separate websocket-only banner line from old path near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1010).
4. Verify LLM request log lines and nostr logs coexist in one log destination.
## Rollout Steps
1. Implement callback infrastructure and migrate websocket logging first.
2. Validate no direct file writes remain in websocket module.
3. Migrate nip013 logging.
4. Add documentation and examples.
5. Update didactyl wiring.
6. Remove obsolete references to `ENABLE_FILE_LOGGING` for websocket logging behavior if no longer needed.
## Risks and Mitigations
- Risk: excessive callback volume from websocket trace traffic.
- Mitigation: implement minimum level filter in library and map websocket traffic to trace level.
- Risk: callback reentrancy or thread concerns.
- Mitigation: document callback must be fast and thread-safe, avoid heavy blocking in callback.
- Risk: API drift across consumers.
- Mitigation: keep callback signature minimal and stable.
## Definition of Done
- No direct `fopen("debug.log", ...)` remains in production logging paths in `nostr_core_lib`.
- Consumer can set callback and receive websocket and nip013 logs.
- Didactyl routes nostr_core_lib logs through its logger and single configured destination.
- Documentation updated with migration and usage examples.
+209
View File
@@ -0,0 +1,209 @@
# NIP-42 Authentication Support for core_relay_pool.c
## Problem Statement
The relay pool ([`core_relay_pool.c`](nostr_core/core_relay_pool.c)) does not handle NIP-42 authentication. When a relay sends an `AUTH` challenge message, the pool silently ignores it, causing:
- Subscriptions to fail on auth-required relays (events never delivered)
- Publishes to be rejected with `auth-required:` errors
- No visibility into authentication state per relay
The synchronous relay functions in [`core_relays.c`](nostr_core/core_relays.c) already have full NIP-42 support (lines 58-63, 211-239, 595-624). The NIP-42 primitives in [`nip042.c`](nostr_core/nip042.c) / [`nip042.h`](nostr_core/nip042.h) are production-ready. This plan wires them into the pool.
## Architecture Overview
```mermaid
flowchart TD
A[Relay sends AUTH challenge] --> B[process_relay_message parses AUTH]
B --> C{pool has private_key?}
C -->|No| D[Log warning, mark relay auth_state = CHALLENGE_RECEIVED]
C -->|Yes| E[nostr_nip42_create_auth_event]
E --> F[nostr_nip42_create_auth_message]
F --> G[nostr_ws_send_text to relay]
G --> H[Mark relay auth_state = AUTHENTICATING]
H --> I[Relay sends OK for auth event]
I --> J[Mark relay auth_state = AUTHENTICATED]
K[Relay sends OK with auth-required] --> L[process_relay_message detects prefix]
L --> M{relay already authenticated?}
M -->|No| N[Trigger auth flow if private_key available]
M -->|Yes| O[Surface as publish failure]
P[Relay sends NOTICE] --> Q{Contains auth-required?}
Q -->|Yes| R[Log auth-required notice]
Q -->|No| S[Ignore or log]
```
## Detailed Changes
### 1. Add NIP-42 include to core_relay_pool.c
Add `#include "nip042.h"` alongside the existing includes at the top of [`core_relay_pool.c`](nostr_core/core_relay_pool.c:27).
### 2. Add auth fields to `relay_connection_t` struct
Modeled after [`core_relays.c:58-63`](nostr_core/core_relays.c:58), add to the [`relay_connection_t`](nostr_core/core_relay_pool.c:76) struct:
```c
// NIP-42 Authentication fields
nostr_auth_state_t auth_state;
char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
time_t auth_challenge_time;
int nip42_enabled;
```
These go after the existing `last_connection_error_time` field (around line 106).
### 3. Add private key and NIP-42 config to `nostr_relay_pool` struct
Add to the [`nostr_relay_pool`](nostr_core/core_relay_pool.c:146) struct:
```c
// NIP-42 Authentication configuration
int nip42_enabled;
unsigned char private_key[32];
int has_private_key;
```
### 4. Add public API: `nostr_relay_pool_set_auth()`
Add to [`nostr_core.h`](nostr_core/nostr_core.h) after the existing pool management functions (around line 252):
```c
// NIP-42 Authentication configuration for relay pool
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
const unsigned char* private_key,
int enable);
```
Implement in [`core_relay_pool.c`](nostr_core/core_relay_pool.c):
- Copies the 32-byte private key into the pool struct
- Sets `nip42_enabled` flag
- Propagates `nip42_enabled` to all existing relay connections
### 5. Handle AUTH in `process_relay_message()`
Add an `AUTH` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912) between the existing `OK` and `PONG` handlers (around line 1097). The logic mirrors [`core_relays.c:211-239`](nostr_core/core_relays.c:211):
```c
} else if (strcmp(msg_type, "AUTH") == 0) {
// Handle AUTH challenge: ["AUTH", <challenge-string>]
if (pool->nip42_enabled && pool->has_private_key &&
cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(challenge_json)) {
const char* challenge = cJSON_GetStringValue(challenge_json);
// Store challenge
strncpy(relay->auth_challenge, challenge,
sizeof(relay->auth_challenge) - 1);
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
relay->auth_challenge_time = time(NULL);
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
// Create and send auth event
cJSON* auth_event = nostr_nip42_create_auth_event(
challenge, relay->url, pool->private_key, 0);
if (auth_event) {
char* auth_message = nostr_nip42_create_auth_message(auth_event);
if (auth_message) {
if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) {
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
}
free(auth_message);
}
cJSON_Delete(auth_event);
}
}
}
}
```
### 6. Handle AUTH in `nostr_relay_pool_query_sync()` inline loop
The [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:1107) function has its own inline message parsing loop (lines 1178-1227) that bypasses `process_relay_message()`. Add AUTH handling there too, similar to the pattern above.
### 7. Handle NOTICE messages
Add a `NOTICE` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912):
```c
} else if (strcmp(msg_type, "NOTICE") == 0) {
// Handle NOTICE: ["NOTICE", <message>]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(notice_msg)) {
const char* notice = cJSON_GetStringValue(notice_msg);
// Detect auth-required notices
if (strstr(notice, "auth-required") != NULL) {
relay->auth_state = NOSTR_AUTH_STATE_NONE; // Reset for re-auth
}
}
}
}
```
### 8. Handle OK auth-required rejections
Enhance the existing [`OK` handler](nostr_core/core_relay_pool.c:1048) to detect `auth-required:` prefix in error messages. When detected:
- If the pool has a private key and the relay hasn't been authenticated yet, trigger the auth flow
- Surface the rejection through the publish callback with the auth-required message
### 9. Initialize auth fields in `nostr_relay_pool_add_relay()`
In [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:605), initialize the new auth fields:
```c
// Initialize NIP-42 authentication fields
relay->auth_state = NOSTR_AUTH_STATE_NONE;
memset(relay->auth_challenge, 0, sizeof(relay->auth_challenge));
relay->auth_challenge_time = 0;
relay->nip42_enabled = pool->nip42_enabled;
```
### 10. Secure cleanup in `nostr_relay_pool_destroy()`
In [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:684), zero the private key before freeing:
```c
// Securely zero private key
if (pool->has_private_key) {
memset(pool->private_key, 0, sizeof(pool->private_key));
pool->has_private_key = 0;
}
```
### 11. Test file
Create `tests/nip42_pool_test.c` that:
- Creates a pool with auth configured
- Verifies `nostr_relay_pool_set_auth()` stores the key
- Simulates AUTH challenge parsing logic (unit-level, no live relay needed)
- Verifies auth event creation with the pool's private key
### 12. Documentation updates
- Update [`POOL_API.md`](POOL_API.md) with `nostr_relay_pool_set_auth()` documentation
- Update [`README.md`](README.md) with a relay pool NIP-42 usage example
## Files Modified
| File | Change Type | Description |
|------|-------------|-------------|
| [`nostr_core/core_relay_pool.c`](nostr_core/core_relay_pool.c) | Modified | Add auth fields, AUTH/NOTICE handlers, auth config API |
| [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h) | Modified | Add `nostr_relay_pool_set_auth()` declaration |
| [`tests/nip42_pool_test.c`](tests/nip42_pool_test.c) | New | NIP-42 pool authentication tests |
| [`POOL_API.md`](POOL_API.md) | Modified | Document new auth API |
| [`README.md`](README.md) | Modified | Add pool auth usage example |
## Key Design Decisions
1. **Private key stored in pool, not per-relay**: A single identity authenticates to all relays in the pool. This matches the common use case and mirrors how `core_relays.c` accepts a single `private_key` parameter.
2. **Auth state tracked per-relay**: Each relay connection tracks its own `nostr_auth_state_t` since different relays may challenge at different times.
3. **Automatic re-auth on reconnect**: When a relay reconnects (via the existing reconnection logic), the auth state resets to `NOSTR_AUTH_STATE_NONE`, allowing the relay to re-challenge.
4. **No blocking on auth**: The AUTH response is sent asynchronously. If a relay requires auth before accepting subscriptions, the relay will re-send events after authentication succeeds. The pool does not block waiting for auth confirmation.
5. **Reuse existing NIP-42 primitives**: All crypto and message formatting uses [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84) — no new crypto code needed.
+350
View File
@@ -0,0 +1,350 @@
# NIP-46 Remote Signing — Implementation Plan
## Overview
NIP-46 defines a protocol for **Nostr Remote Signing**, enabling 2-way communication between a client application and a remote signer (bunker) over Nostr relays. The remote signer holds the user's private keys and performs cryptographic operations on behalf of the client, reducing the attack surface by keeping keys off the client device.
This plan covers implementing **both sides** of the protocol:
- **Client-side**: sends requests to a remote signer and processes responses
- **Remote-signer-side**: receives requests and produces responses (for building bunker applications)
## Architecture
### Protocol Flow
```mermaid
sequenceDiagram
participant C as Client
participant R as Relay
participant S as Remote Signer
Note over C: Generate client-keypair
C->>R: Subscribe to kind:24133 p-tagged to client-pubkey
C->>R: Publish connect request, kind:24133, p-tag remote-signer-pubkey
R->>S: Deliver connect request
S->>R: Publish connect response, kind:24133, p-tag client-pubkey
R->>C: Deliver connect response
Note over C: Connection established
C->>R: Publish get_public_key request
R->>S: Deliver request
S->>R: Publish response with user-pubkey
R->>C: Deliver response
Note over C: Now knows user-pubkey
C->>R: Publish sign_event request
R->>S: Deliver request
S->>R: Publish signed event response
R->>C: Deliver signed event
```
### Connection Initiation
```mermaid
flowchart TD
A[Connection Initiation] --> B{Who initiates?}
B -->|Remote Signer| C[bunker:// URL]
B -->|Client| D[nostrconnect:// URL]
C --> E[Client parses bunker URL]
E --> F[Extract remote-signer-pubkey + relays + secret]
F --> G[Client sends connect request to remote-signer]
D --> H[Remote signer parses nostrconnect URL]
H --> I[Extract client-pubkey + relays + secret + perms]
I --> J[Remote signer sends connect response to client]
G --> K[Connection Established]
J --> K
```
### Component Dependency Map
```mermaid
graph TD
NIP46[nip046.c/h] --> NIP44[nip044 - NIP-44 Encryption]
NIP46 --> NIP04[nip004 - NIP-04 Encryption]
NIP46 --> NIP01[nip001 - Event Creation/Signing]
NIP46 --> POOL[core_relay_pool - Relay Communication]
NIP46 --> UTILS[utils - Hex/Bytes/Random]
NIP46 --> CJSON[cJSON - JSON Handling]
NIP46 --> COMMON[nostr_common - Error Codes/Constants]
```
## Data Structures
### Core Types
```c
// NIP-46 event kind
#define NOSTR_NIP46_REQUEST_KIND 24133
// Connection types
typedef enum {
NOSTR_NIP46_CONN_BUNKER, // bunker:// initiated by remote-signer
NOSTR_NIP46_CONN_NOSTRCONNECT // nostrconnect:// initiated by client
} nostr_nip46_connection_type_t;
// RPC Method types
typedef enum {
NOSTR_NIP46_METHOD_CONNECT,
NOSTR_NIP46_METHOD_SIGN_EVENT,
NOSTR_NIP46_METHOD_PING,
NOSTR_NIP46_METHOD_GET_PUBLIC_KEY,
NOSTR_NIP46_METHOD_NIP04_ENCRYPT,
NOSTR_NIP46_METHOD_NIP04_DECRYPT,
NOSTR_NIP46_METHOD_NIP44_ENCRYPT,
NOSTR_NIP46_METHOD_NIP44_DECRYPT
} nostr_nip46_method_t;
// Parsed bunker:// URL
typedef struct {
char remote_signer_pubkey[65]; // hex pubkey
char relays[8][256]; // up to 8 relay URLs
int relay_count;
char secret[128]; // optional secret
} nostr_nip46_bunker_url_t;
// Parsed nostrconnect:// URL
typedef struct {
char client_pubkey[65]; // hex pubkey
char relays[8][256]; // relay URLs
int relay_count;
char secret[128]; // required secret
char perms[512]; // optional permissions
char name[128]; // optional app name
char url[256]; // optional app URL
char image[256]; // optional app image
} nostr_nip46_nostrconnect_url_t;
// JSON-RPC Request
typedef struct {
char id[65]; // random request ID
nostr_nip46_method_t method;
char method_str[32]; // string form of method
char** params; // array of string params
int param_count;
} nostr_nip46_request_t;
// JSON-RPC Response
typedef struct {
char id[65]; // matching request ID
char* result; // result string
char* error; // error string, NULL if success
} nostr_nip46_response_t;
// Client session state
typedef struct {
unsigned char client_private_key[32];
char client_pubkey_hex[65];
char remote_signer_pubkey_hex[65];
unsigned char remote_signer_pubkey[32];
char user_pubkey_hex[65]; // learned via get_public_key
char relays[8][256];
int relay_count;
int connected;
} nostr_nip46_client_session_t;
// Signer session state
typedef struct {
unsigned char signer_private_key[32];
char signer_pubkey_hex[65];
unsigned char user_private_key[32];
char user_pubkey_hex[65];
char client_pubkey_hex[65];
unsigned char client_pubkey[32];
char relays[8][256];
int relay_count;
int connected;
} nostr_nip46_signer_session_t;
```
## Public API Functions
### URL Parsing
| Function | Description |
|----------|-------------|
| `nostr_nip46_parse_bunker_url()` | Parse `bunker://<pubkey>?relay=...&secret=...` |
| `nostr_nip46_parse_nostrconnect_url()` | Parse `nostrconnect://<pubkey>?relay=...&secret=...&perms=...` |
| `nostr_nip46_create_bunker_url()` | Generate a bunker:// connection token |
| `nostr_nip46_create_nostrconnect_url()` | Generate a nostrconnect:// connection token |
### JSON-RPC Message Construction
| Function | Description |
|----------|-------------|
| `nostr_nip46_create_request()` | Build a JSON-RPC request object |
| `nostr_nip46_create_response()` | Build a JSON-RPC response object |
| `nostr_nip46_parse_request()` | Parse decrypted JSON into request struct |
| `nostr_nip46_parse_response()` | Parse decrypted JSON into response struct |
| `nostr_nip46_free_request()` | Free request struct memory |
| `nostr_nip46_free_response()` | Free response struct memory |
### Event Construction (kind: 24133)
| Function | Description |
|----------|-------------|
| `nostr_nip46_create_request_event()` | Create NIP-44 encrypted kind:24133 request event |
| `nostr_nip46_create_response_event()` | Create NIP-44 encrypted kind:24133 response event |
| `nostr_nip46_decrypt_event()` | Decrypt a kind:24133 event content |
### Client-Side Operations
| Function | Description |
|----------|-------------|
| `nostr_nip46_client_session_init()` | Initialize client session from bunker URL |
| `nostr_nip46_client_session_destroy()` | Clean up client session |
| `nostr_nip46_client_connect()` | Send connect request to remote signer |
| `nostr_nip46_client_get_public_key()` | Request user pubkey from remote signer |
| `nostr_nip46_client_sign_event()` | Request event signing from remote signer |
| `nostr_nip46_client_ping()` | Send ping to remote signer |
| `nostr_nip46_client_nip04_encrypt()` | Request NIP-04 encryption |
| `nostr_nip46_client_nip04_decrypt()` | Request NIP-04 decryption |
| `nostr_nip46_client_nip44_encrypt()` | Request NIP-44 encryption |
| `nostr_nip46_client_nip44_decrypt()` | Request NIP-44 decryption |
### Remote-Signer-Side Operations
| Function | Description |
|----------|-------------|
| `nostr_nip46_signer_session_init()` | Initialize signer session |
| `nostr_nip46_signer_session_destroy()` | Clean up signer session |
| `nostr_nip46_signer_handle_request()` | Process an incoming request and produce a response |
| `nostr_nip46_signer_create_bunker_url()` | Generate bunker URL for distribution |
### Utility
| Function | Description |
|----------|-------------|
| `nostr_nip46_generate_request_id()` | Generate random request ID string |
| `nostr_nip46_method_to_string()` | Convert method enum to string |
| `nostr_nip46_string_to_method()` | Convert string to method enum |
## Error Codes
New error codes to add to `nostr_common.h`:
```c
// NIP-46 Remote Signing error codes
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
#define NOSTR_ERROR_NIP46_TIMEOUT -307
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
```
## Files to Create/Modify
### New Files
| File | Purpose |
|------|---------|
| `nostr_core/nip046.h` | Header with all NIP-46 types, constants, and function declarations |
| `nostr_core/nip046.c` | Implementation of all NIP-46 functions |
| `tests/nip46_test.c` | Comprehensive test suite |
| `examples/nip46_remote_signer.c` | Example showing both client and signer usage |
### Modified Files
| File | Change |
|------|--------|
| `nostr_core/nostr_core.h` | Add `#include "nip046.h"` and NIP-46 API docs in header comment |
| `nostr_core/nostr_common.h` | Add NIP-46 error code defines |
| `build.sh` | Add NIP-046 to auto-detection, `--nips=all` list, and description mapping |
## Implementation Details
### URL Parsing Strategy
The `bunker://` and `nostrconnect://` URLs use standard URI format with query parameters. Implementation will:
1. Validate the scheme prefix
2. Extract the pubkey from the authority section
3. Parse query parameters using simple string splitting — no external URL parsing library needed
4. URL-decode relay values since they contain `://` characters
### NIP-44 Encryption Integration
All kind:24133 event content is NIP-44 encrypted. The implementation will use the existing `nostr_nip44_encrypt()` and `nostr_nip44_decrypt()` functions from `nip044.h`. The flow:
1. Build JSON-RPC payload as a string
2. Encrypt with `nostr_nip44_encrypt(client_privkey, remote_signer_pubkey, payload, ...)`
3. Set as event content
4. On receive: `nostr_nip44_decrypt(my_privkey, sender_pubkey, content, ...)`
5. Parse decrypted JSON-RPC payload
### Relay Communication
For the initial implementation, the NIP-46 module will provide **event construction and parsing only** — it will not manage relay connections directly. Users will use the existing relay pool API to:
1. Subscribe to kind:24133 events p-tagged to their pubkey
2. Publish kind:24133 request/response events
This keeps the module focused and composable, matching the pattern used by NIP-59 and NIP-17.
A higher-level convenience layer could be added later that wraps the relay pool for a fully managed NIP-46 session.
### Auth Challenge Handling
When a response has `result: "auth_url"`, the error field contains a URL. The client-side API will return a specific error code `NOSTR_ERROR_NIP46_AUTH_CHALLENGE` and provide the URL in the response struct's error field, allowing the application to handle it appropriately.
## Test Plan
The test suite will cover:
1. **URL Parsing Tests**
- Parse valid bunker:// URLs with single and multiple relays
- Parse valid nostrconnect:// URLs with all optional fields
- Reject malformed URLs
- Handle URL-encoded relay values
2. **URL Generation Tests**
- Generate bunker:// URLs and verify round-trip parsing
- Generate nostrconnect:// URLs and verify round-trip parsing
3. **JSON-RPC Message Tests**
- Create and parse each method type request
- Create and parse success responses
- Create and parse error responses
- Verify request ID matching
4. **Event Construction Tests**
- Create kind:24133 request events with proper encryption
- Create kind:24133 response events with proper encryption
- Decrypt events and verify content matches
5. **Method Enum Conversion Tests**
- Round-trip all method enum values through string conversion
6. **Signer Request Handling Tests**
- Handle connect request and produce ack response
- Handle sign_event request and produce signed event response
- Handle ping request and produce pong response
- Handle get_public_key and return correct pubkey
- Handle NIP-04/NIP-44 encrypt/decrypt requests
7. **End-to-End Flow Tests**
- Client creates connect request → Signer handles → Client processes response
- Client creates sign_event request → Signer signs → Client gets signed event
- Full bunker:// connection flow
- Secret validation on connect
## Implementation Order
1. Error codes in `nostr_common.h`
2. Header file `nip046.h` with all declarations
3. URL parsing functions
4. JSON-RPC request/response construction and parsing
5. Kind:24133 event creation and decryption
6. Client session management
7. Signer session and request handling
8. Utility functions
9. Build system updates
10. Test suite
11. Example program
12. Documentation updates in `nostr_core.h`
+778
View File
@@ -0,0 +1,778 @@
# NIP-60 Cashu Wallet & NIP-61 Nutzaps — Implementation Plan
## Overview
**NIP-60** defines a protocol for storing Cashu wallet state on Nostr relays, enabling cross-application wallet portability. The wallet stores unspent proofs as encrypted events, tracks spending history, and manages mint relationships.
**NIP-61** defines "Nutzaps" — P2PK-locked Cashu tokens sent as zaps between Nostr users, where the payment itself serves as the receipt.
This implementation covers:
- **NIP-60**: Wallet event creation/parsing (kind:17375), token events (kind:7375), spending history (kind:7376), quote events (kind:7374)
- **NIP-61**: Nutzap info events (kind:10019), nutzap events (kind:9321), nutzap redemption
- **Cashu Mint HTTP Client**: Mint info, token swap, melt, and mint operations via curl
## Architecture
### Component Dependency Map
```mermaid
graph TD
NIP60[nip060.c/h - Cashu Wallet Events] --> NIP44[nip044 - NIP-44 Encryption]
NIP60 --> NIP01[nip001 - Event Creation/Signing]
NIP60 --> CJSON[cJSON - JSON Handling]
NIP60 --> UTILS[utils - Hex/Bytes/Base64]
NIP60 --> COMMON[nostr_common - Error Codes]
NIP61[nip061.c/h - Nutzaps] --> NIP44
NIP61 --> NIP01
NIP61 --> CJSON
NIP61 --> UTILS
NIP61 --> COMMON
CASHU[cashu_mint.c/h - Mint HTTP Client] --> CJSON
CASHU --> CURL[libcurl - HTTP]
CASHU --> COMMON
NIP60 -.->|optional| CASHU
NIP61 -.->|optional| CASHU
```
### NIP-60 High-Level Flow
```mermaid
sequenceDiagram
participant App as Application
participant W as NIP-60 Wallet Lib
participant R as Relay
participant M as Cashu Mint
Note over App: Initialize wallet
App->>W: nostr_nip60_create_wallet_event
W->>R: Publish kind:17375
Note over App: Receive tokens
App->>M: Mint tokens via cashu_mint_mint_tokens
M-->>App: Cashu proofs
App->>W: nostr_nip60_create_token_event
W->>R: Publish kind:7375
Note over App: Spend tokens
App->>W: nostr_nip60_spend_token
W->>M: Swap proofs via cashu_mint_swap
M-->>W: Change proofs
W->>R: Delete old kind:7375
W->>R: Publish new kind:7375 with change
W->>R: Publish kind:7376 history
```
### NIP-61 Nutzap Flow
```mermaid
sequenceDiagram
participant Alice as Alice - Sender
participant R as Relay
participant Bob as Bob - Recipient
participant M as Cashu Mint
Note over Bob: Setup
Bob->>R: Publish kind:10019 with mints + p2pk pubkey
Note over Alice: Send nutzap
Alice->>R: Fetch Bobs kind:10019
Alice->>M: Mint/swap P2PK-locked tokens
Alice->>R: Publish kind:9321 nutzap
Note over Bob: Receive nutzap
Bob->>R: Fetch kind:9321 events
Bob->>M: Swap P2PK tokens into wallet
Bob->>R: Publish kind:7376 redemption history
```
### Token State Transition
```mermaid
stateDiagram-v2
[*] --> Created: Mint/Receive tokens
Created --> Published: Publish kind 7375
Published --> Spending: Spend some proofs
Spending --> Deleted: NIP-09 delete old event
Spending --> RolledOver: New kind 7375 with unspent + change
RolledOver --> Published
Deleted --> [*]
Published --> Redeemed: All proofs spent
Redeemed --> Deleted
```
## File Structure
```
nostr_core/
nip060.h # NIP-60 Cashu wallet types and function declarations
nip060.c # NIP-60 implementation
nip061.h # NIP-61 Nutzap types and function declarations
nip061.c # NIP-61 implementation
cashu_mint.h # Cashu mint HTTP client types and functions
cashu_mint.c # Cashu mint HTTP client implementation
tests/
nip60_test.c # NIP-60 unit tests
nip61_test.c # NIP-61 unit tests
cashu_mint_test.c # Cashu mint client tests
examples/
cashu_wallet.c # Complete wallet example
```
## Data Structures
### NIP-60 Core Types
```c
// Event kinds
#define NOSTR_NIP60_WALLET_KIND 17375
#define NOSTR_NIP60_TOKEN_KIND 7375
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
// A single Cashu proof
typedef struct {
char id[17]; // Keyset ID, e.g. "005c2502034d4f12"
uint64_t amount; // Denomination amount
char* secret; // Proof secret (base64 or P2PK JSON)
char* C; // Blinded signature (hex compressed point)
} nostr_cashu_proof_t;
// Token content - decrypted payload of kind:7375
typedef struct {
char* mint_url; // Mint URL
nostr_cashu_proof_t* proofs; // Array of proofs
int proof_count; // Number of proofs
char** deleted_token_ids; // Event IDs of tokens destroyed
int deleted_count; // Number of deleted token IDs
} nostr_nip60_token_data_t;
// Wallet content - decrypted payload of kind:17375
typedef struct {
char privkey[65]; // Hex private key for P2PK
char** mint_urls; // Array of mint URLs
int mint_count; // Number of mints
} nostr_nip60_wallet_data_t;
// Spending history entry direction
typedef enum {
NOSTR_NIP60_DIRECTION_IN, // Received funds
NOSTR_NIP60_DIRECTION_OUT // Sent funds
} nostr_nip60_direction_t;
// History event reference
typedef enum {
NOSTR_NIP60_REF_CREATED, // New token event created
NOSTR_NIP60_REF_DESTROYED, // Token event destroyed
NOSTR_NIP60_REF_REDEEMED // Nutzap redeemed
} nostr_nip60_ref_marker_t;
typedef struct {
char event_id[65]; // Referenced event ID
char relay_hint[256]; // Optional relay hint
nostr_nip60_ref_marker_t marker; // Reference type
} nostr_nip60_history_ref_t;
// Spending history data - decrypted payload of kind:7376
typedef struct {
nostr_nip60_direction_t direction; // in or out
uint64_t amount; // Amount in sats
nostr_nip60_history_ref_t* refs; // Event references
int ref_count; // Number of references
} nostr_nip60_history_data_t;
```
### NIP-61 Core Types
```c
// Event kinds
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
#define NOSTR_NIP61_NUTZAP_KIND 9321
// Mint entry in kind:10019
typedef struct {
char* url; // Mint URL
char** units; // Supported units, e.g. "sat", "usd"
int unit_count; // Number of units
} nostr_nip61_mint_entry_t;
// Nutzap info - parsed kind:10019
typedef struct {
char** relay_urls; // Relays for receiving nutzaps
int relay_count;
nostr_nip61_mint_entry_t* mints; // Trusted mints
int mint_count;
char pubkey[67]; // P2PK pubkey (with 02 prefix)
} nostr_nip61_nutzap_info_t;
// Nutzap event data - parsed kind:9321
typedef struct {
char* content; // Optional comment
nostr_cashu_proof_t* proofs; // P2PK-locked proofs
int proof_count;
char* mint_url; // Mint URL
char recipient_pubkey[65]; // Recipient Nostr pubkey
char nutzapped_event_id[65]; // Event being nutzapped (optional)
char nutzapped_relay_hint[256]; // Relay hint for nutzapped event
int nutzapped_kind; // Kind of nutzapped event
} nostr_nip61_nutzap_data_t;
```
### Cashu Mint Client Types
```c
// Cashu protocol version
#define CASHU_API_VERSION "v1"
// Mint keyset
typedef struct {
char id[17]; // Keyset ID
char unit[8]; // Unit, e.g. "sat"
int active; // Whether keyset is active
uint64_t* amounts; // Denomination amounts
char** pubkeys; // Corresponding public keys
int key_count; // Number of keys
} cashu_keyset_t;
// Mint info
typedef struct {
char* name;
char* description;
char* version;
char** supported_nuts; // NUT numbers supported
int nut_count;
cashu_keyset_t* keysets;
int keyset_count;
} cashu_mint_info_t;
// Mint quote (for minting new tokens)
typedef struct {
char quote_id[128];
char payment_request[1024]; // Lightning invoice
int paid;
uint64_t amount;
time_t expiry;
} cashu_mint_quote_t;
// Melt quote (for paying Lightning invoices)
typedef struct {
char quote_id[128];
uint64_t amount;
uint64_t fee_reserve;
int paid;
char* payment_preimage;
time_t expiry;
} cashu_melt_quote_t;
// Blind message for minting/swapping
typedef struct {
uint64_t amount;
char* B_; // Blinded secret (hex)
char id[17]; // Keyset ID
} cashu_blinded_message_t;
// Blind signature from mint
typedef struct {
uint64_t amount;
char* C_; // Blinded signature (hex)
char id[17]; // Keyset ID
} cashu_blind_signature_t;
```
## API Functions
### NIP-60 Wallet Functions
```c
// === Wallet Event (kind:17375) ===
// Create a wallet event with encrypted content
cJSON* nostr_nip60_create_wallet_event(
const nostr_nip60_wallet_data_t* wallet_data,
const unsigned char* private_key,
time_t timestamp);
// Parse and decrypt a wallet event
int nostr_nip60_parse_wallet_event(
cJSON* event,
const unsigned char* private_key,
nostr_nip60_wallet_data_t* wallet_data_out);
// Free wallet data
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data);
// === Token Event (kind:7375) ===
// Create a token event with encrypted proofs
cJSON* nostr_nip60_create_token_event(
const nostr_nip60_token_data_t* token_data,
const unsigned char* private_key,
time_t timestamp);
// Parse and decrypt a token event
int nostr_nip60_parse_token_event(
cJSON* event,
const unsigned char* private_key,
nostr_nip60_token_data_t* token_data_out);
// Free token data
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data);
// === Token Spending ===
// Create a NIP-09 deletion event for a spent token
cJSON* nostr_nip60_create_token_deletion(
const char* token_event_id,
const unsigned char* private_key,
time_t timestamp);
// Create a rollover token event (unspent proofs + change from old token)
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);
// === Spending History (kind:7376) ===
// Create a spending history event
cJSON* nostr_nip60_create_history_event(
const nostr_nip60_history_data_t* history_data,
const unsigned char* private_key,
time_t timestamp);
// Parse and decrypt a spending history event
int nostr_nip60_parse_history_event(
cJSON* event,
const unsigned char* private_key,
nostr_nip60_history_data_t* history_data_out);
// Free history data
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data);
// === Quote Event (kind:7374) ===
// Create a mint quote event
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);
// Parse a quote event
int nostr_nip60_parse_quote_event(
cJSON* event,
const unsigned char* private_key,
char* quote_id_out,
size_t quote_id_size,
char* mint_url_out,
size_t mint_url_size);
// === Utility: Proof helpers ===
// Calculate total amount from an array of proofs
uint64_t nostr_nip60_sum_proofs(
const nostr_cashu_proof_t* proofs,
int proof_count);
// Serialize proofs to JSON array
cJSON* nostr_nip60_proofs_to_json(
const nostr_cashu_proof_t* proofs,
int proof_count);
// Parse proofs from JSON array
int nostr_nip60_proofs_from_json(
cJSON* json_array,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out);
// Free proof array
void nostr_nip60_free_proofs(
nostr_cashu_proof_t* proofs,
int proof_count);
// === Filter helpers ===
// Create a filter to fetch wallet and token events
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex);
// Create a filter to fetch spending history
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since);
```
### NIP-61 Nutzap Functions
```c
// === Nutzap Info Event (kind:10019) ===
// Create nutzap info event
cJSON* nostr_nip61_create_nutzap_info_event(
const nostr_nip61_nutzap_info_t* info,
const unsigned char* private_key,
time_t timestamp);
// Parse nutzap info event
int nostr_nip61_parse_nutzap_info_event(
cJSON* event,
nostr_nip61_nutzap_info_t* info_out);
// Free nutzap info
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
// === Nutzap Event (kind:9321) ===
// Create a nutzap event
cJSON* nostr_nip61_create_nutzap_event(
const nostr_nip61_nutzap_data_t* nutzap_data,
const unsigned char* sender_private_key,
time_t timestamp);
// Parse a nutzap event
int nostr_nip61_parse_nutzap_event(
cJSON* event,
nostr_nip61_nutzap_data_t* nutzap_data_out);
// Free nutzap data
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data);
// === Nutzap Redemption ===
// Create a redemption history event (kind:7376 with redeemed marker)
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);
// === Nutzap Verification ===
// Verify a nutzap against recipients kind:10019
int nostr_nip61_verify_nutzap(
cJSON* nutzap_event,
cJSON* nutzap_info_event);
// === Filter helpers ===
// Create filter to fetch nutzap info for a user
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
// Create filter to fetch incoming nutzaps
cJSON* nostr_nip61_create_nutzap_filter(
const char* recipient_pubkey_hex,
const char** mint_urls,
int mint_count,
time_t since);
```
### Cashu Mint HTTP Client Functions
```c
// === Mint Connection ===
// Get mint info
int cashu_mint_get_info(
const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds);
// Free mint info
void cashu_mint_free_info(cashu_mint_info_t* info);
// === Keysets ===
// Get active keysets from mint
int cashu_mint_get_keysets(
const char* mint_url,
cashu_keyset_t** keysets_out,
int* keyset_count_out,
int timeout_seconds);
// Get specific keyset keys
int cashu_mint_get_keys(
const char* mint_url,
const char* keyset_id,
cashu_keyset_t* keyset_out,
int timeout_seconds);
// Free keysets
void cashu_mint_free_keysets(cashu_keyset_t* keysets, int count);
// === Minting (receiving Lightning) ===
// Request a mint quote (get Lightning invoice)
int cashu_mint_request_mint_quote(
const char* mint_url,
uint64_t amount,
const char* unit,
cashu_mint_quote_t* quote_out,
int timeout_seconds);
// Check mint quote status
int cashu_mint_check_mint_quote(
const char* mint_url,
const char* quote_id,
cashu_mint_quote_t* quote_out,
int timeout_seconds);
// Mint tokens (after quote is paid)
int cashu_mint_mint_tokens(
const char* mint_url,
const char* quote_id,
const cashu_blinded_message_t* blinded_messages,
int message_count,
cashu_blind_signature_t** signatures_out,
int* signature_count_out,
int timeout_seconds);
// === Melting (paying Lightning) ===
// Request a melt quote (estimate fee for paying invoice)
int cashu_mint_request_melt_quote(
const char* mint_url,
const char* payment_request,
const char* unit,
cashu_melt_quote_t* quote_out,
int timeout_seconds);
// Check melt quote status
int cashu_mint_check_melt_quote(
const char* mint_url,
const char* quote_id,
cashu_melt_quote_t* quote_out,
int timeout_seconds);
// Melt tokens (pay Lightning invoice)
int cashu_mint_melt_tokens(
const char* mint_url,
const char* quote_id,
const nostr_cashu_proof_t* proofs,
int proof_count,
cashu_melt_quote_t* result_out,
cashu_blind_signature_t** change_out,
int* change_count_out,
int timeout_seconds);
// === Swapping ===
// Swap proofs (split/combine denominations)
int cashu_mint_swap(
const char* mint_url,
const nostr_cashu_proof_t* inputs,
int input_count,
const cashu_blinded_message_t* outputs,
int output_count,
cashu_blind_signature_t** signatures_out,
int* signature_count_out,
int timeout_seconds);
// === Token State ===
// Check if proofs have been spent
int cashu_mint_check_proofs_state(
const char* mint_url,
const nostr_cashu_proof_t* proofs,
int proof_count,
int* states_out,
int timeout_seconds);
// === Crypto Helpers ===
// Generate blinded messages for a target amount
int cashu_create_blinded_messages(
uint64_t amount,
const char* keyset_id,
cashu_blinded_message_t** messages_out,
int* message_count_out,
char*** secrets_out,
char*** rs_out);
// Unblind signatures to get proofs
int cashu_unblind_signatures(
const cashu_blind_signature_t* signatures,
int signature_count,
const char** secrets,
const char** rs,
const cashu_keyset_t* keyset,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out);
// Create P2PK-locked blinded messages
int cashu_create_p2pk_blinded_messages(
uint64_t amount,
const char* keyset_id,
const char* recipient_pubkey_hex,
cashu_blinded_message_t** messages_out,
int* message_count_out,
char*** secrets_out,
char*** rs_out);
// Free blinded messages
void cashu_free_blinded_messages(cashu_blinded_message_t* messages, int count);
// Free blind signatures
void cashu_free_blind_signatures(cashu_blind_signature_t* signatures, int count);
```
## Error Codes
New error codes to add to `nostr_common.h`:
```c
// NIP-60 Cashu Wallet error codes
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
// NIP-61 Nutzap error codes
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
// Cashu Mint client error codes
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
```
## Build System Changes
### build.sh Updates
1. Add `060` and `061` to the NIP detection patterns
2. Add `cashu_mint.c` as a source when NIP-60 or NIP-61 is detected
3. Add NIP descriptions: `NIP-060(Cashu-Wallet)`, `NIP-061(Nutzaps)`
4. Update the `--nips=all` list to include `060` and `061`
5. Update the help text with new NIP descriptions
### nostr_core.h Updates
Add includes:
```c
#include "nip060.h" // Cashu Wallet
#include "nip061.h" // Nutzaps
#include "cashu_mint.h" // Cashu Mint HTTP Client
```
Add NIP-60/61 functions to the quick reference comment block.
## Implementation Order
The implementation should proceed in this order due to dependencies:
1. **Error codes** in `nostr_common.h` — no dependencies
2. **Cashu proof types** — shared between NIP-60 and NIP-61
3. **`cashu_mint.h/c`** — Cashu mint HTTP client (depends on curl, cJSON)
4. **`nip060.h/c`** — NIP-60 wallet events (depends on NIP-44, NIP-01, cashu types)
5. **`nip061.h/c`** — NIP-61 nutzaps (depends on NIP-60 types, cashu types)
6. **Build system** — update `build.sh` and `nostr_core.h`
7. **Tests**`nip60_test.c`, `nip61_test.c`, `cashu_mint_test.c`
8. **Example**`cashu_wallet.c`
## Key Design Decisions
### 1. Self-encryption for NIP-44
NIP-60 encrypts content to the user's own key (sender = recipient). The NIP-44 encrypt/decrypt functions take sender_private_key and recipient_public_key. For self-encryption, we derive the user's public key from their private key and use that as the recipient.
### 2. Cashu Crypto (Blinding)
The Cashu protocol requires elliptic curve blinding operations (hash-to-curve, blind/unblind). These use secp256k1 which is already a dependency. The blinding math is:
- `B_ = Y + r*G` where Y = hash_to_curve(secret), r = random blinding factor
- `C_ = k*B_` (mint signs)
- `C = C_ - r*K` where K = mint's public key (unblind)
### 3. P2PK Secrets
For NIP-61 nutzaps, proof secrets use the P2PK format:
```json
["P2PK", {"nonce": "<random>", "data": "02<pubkey>"}]
```
The `02` prefix is required for nostr-cashu compatibility.
### 4. Curl Usage
Following the same pattern as `nip005.c` — direct curl usage with write callbacks. No abstraction layer needed since curl is already a project dependency.
## Test Strategy
### NIP-60 Tests
- Create and parse wallet events (round-trip encryption)
- Create and parse token events with multiple proofs
- Token deletion event creation
- Rollover token creation with del references
- History event creation and parsing
- Quote event creation and parsing
- Proof sum calculation
- Filter creation helpers
### NIP-61 Tests
- Create and parse nutzap info events
- Create and parse nutzap events
- Nutzap verification against kind:10019
- Redemption history event creation
- Filter creation helpers
### Cashu Mint Tests
- Mint info parsing from JSON
- Keyset parsing
- Blinded message creation
- Signature unblinding
- P2PK secret generation
- Proof state checking (mock or live mint)
## Example Usage
```c
#include "nostr_core/nostr_core.h"
// Create a wallet
nostr_nip60_wallet_data_t wallet = {0};
strcpy(wallet.privkey, "<generated-hex-privkey>");
wallet.mint_urls = (char*[]){"https://mint.example.com"};
wallet.mint_count = 1;
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet, my_privkey, 0);
// Publish wallet_event to relays...
// Store tokens
nostr_cashu_proof_t proofs[2] = {
{.id = "00ad268c4d1f5826", .amount = 1, .secret = "...", .C = "02..."},
{.id = "00ad268c4d1f5826", .amount = 4, .secret = "...", .C = "02..."}
};
nostr_nip60_token_data_t token = {
.mint_url = "https://mint.example.com",
.proofs = proofs,
.proof_count = 2
};
cJSON* token_event = nostr_nip60_create_token_event(&token, my_privkey, 0);
// Publish token_event to relays...
// Send a nutzap
nostr_nip61_nutzap_data_t nutzap = {
.content = "Great post!",
.proofs = p2pk_locked_proofs,
.proof_count = 1,
.mint_url = "https://mint.example.com",
.recipient_pubkey = "<bob-pubkey>",
.nutzapped_event_id = "<event-id>"
};
cJSON* nutzap_event = nostr_nip61_create_nutzap_event(&nutzap, my_privkey, 0);
// Publish nutzap_event to relays...
```
+47
View File
@@ -0,0 +1,47 @@
[Tue Oct 7 05:51:04 2025] 🚀 Pool test started
[Tue Oct 7 05:51:07 2025] 🏊 Pool started with default relay
[Tue Oct 7 05:52:03 2025] 🔍 New subscription created (ID: 1)
Filter: {
"limit": 10
}
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: 8433206a6e00...
├── Pubkey: 17323141f3a9...
├── Kind: 1
├── Created: 1759687410
└── Content: Test post at 2025-10-05 14:03:30
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: ec98292f5700...
├── Pubkey: aa3b44608a9e...
├── Kind: 1
├── Created: 1759687283
└── Content: Test post at 2025-10-05 14:01:23
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: c70d6c5c8745...
├── Pubkey: 2a0c81450868...
├── Kind: 1
├── Created: 1759687249
└── Content: Test post at 2025-10-05 14:00:49
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: 15dbe2cfe923...
├── Pubkey: 7c2065299249...
├── Kind: 1
├── Created: 1759687219
└── Content: Test post at 2025-10-05 14:00:19
[Tue Oct 7 05:52:03 2025] 📋 EOSE received - 0 events collected
[Tue Oct 7 05:52:31 2025] 🔍 New subscription created (ID: 2)
Filter: {
"since": 1759830747,
"limit": 10
}
[Tue Oct 7 05:52:31 2025] 📋 EOSE received - 0 events collected
-2579
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More