Compare commits
11
Commits
a8dc2ed046
...
adc7aa9dcf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc7aa9dcf | ||
|
|
1a549afa19 | ||
|
|
6ee62e6575 | ||
|
|
5afe802e8f | ||
|
|
e77973aea0 | ||
|
|
ea84795911 | ||
|
|
481bf3158d | ||
|
|
23e68bdc83 | ||
|
|
4dc753cdcc | ||
|
|
64b7c7ec33 | ||
|
|
f3068f82f3 |
@@ -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"
|
||||||
+28
-6
@@ -6,11 +6,12 @@ This document describes the public API for the Nostr Relay Pool implementation i
|
|||||||
|
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219) | Create and initialize a new relay pool |
|
| [`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:304) | Destroy pool and cleanup all resources |
|
| [`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:229) | Add a relay URL to the pool |
|
| [`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:273) | Remove a relay URL from the pool |
|
| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:702) | Remove a relay URL from the pool |
|
||||||
| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399) | Create async subscription with callbacks |
|
| [`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_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_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_poll()`](nostr_core/core_relay_pool.c:1232) | Single iteration poll and dispatch |
|
||||||
@@ -71,6 +72,26 @@ void cleanup_pool(nostr_relay_pool_t* pool) {
|
|||||||
|
|
||||||
## Relay Management
|
## 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
|
### Add Relay
|
||||||
**Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229)
|
**Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229)
|
||||||
```c
|
```c
|
||||||
@@ -751,5 +772,6 @@ int main() {
|
|||||||
- **Memory ownership**: The pool duplicates filters and URLs internally. Caller owns returned events and must free them.
|
- **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.
|
- **Event deduplication** is applied pool-wide using a circular buffer of 1000 event IDs.
|
||||||
- **Ping functionality** is currently disabled in this build.
|
- **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.
|
- **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:1192) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) to receive events.
|
- **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.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A C library for NOSTR protocol implementation. Work in progress.
|
A C library for NOSTR protocol implementation. Work in progress.
|
||||||
|
|
||||||
[](VERSION)
|
[](VERSION)
|
||||||
[](#license)
|
[](#license)
|
||||||
[](#building)
|
[](#building)
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
|||||||
- [x] [NIP-42](nips/42.md) - Authentication of clients to relays
|
- [x] [NIP-42](nips/42.md) - Authentication of clients to relays
|
||||||
- [x] [NIP-44](nips/44.md) - Versioned Encryption
|
- [x] [NIP-44](nips/44.md) - Versioned Encryption
|
||||||
- [ ] [NIP-45](nips/45.md) - Counting results
|
- [ ] [NIP-45](nips/45.md) - Counting results
|
||||||
- [ ] [NIP-46](nips/46.md) - Nostr Connect
|
- [x] [NIP-46](nips/46.md) - Nostr Remote Signing
|
||||||
- [ ] [NIP-47](nips/47.md) - Wallet Connect
|
- [ ] [NIP-47](nips/47.md) - Wallet Connect
|
||||||
- [ ] [NIP-48](nips/48.md) - Proxy Tags
|
- [ ] [NIP-48](nips/48.md) - Proxy Tags
|
||||||
- [ ] [NIP-49](nips/49.md) - Private Key Encryption
|
- [ ] [NIP-49](nips/49.md) - Private Key Encryption
|
||||||
@@ -96,7 +96,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
|||||||
|
|
||||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
|
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
|
||||||
|
|
||||||
**Implementation Summary:** 12 of 96+ NIPs fully implemented (12.5%)
|
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
|
||||||
|
|
||||||
|
|
||||||
## 📦 Quick Start
|
## 📦 Quick Start
|
||||||
@@ -111,12 +111,12 @@ A C library for NOSTR protocol implementation. Work in progress.
|
|||||||
|
|
||||||
2. **Build the library:**
|
2. **Build the library:**
|
||||||
```bash
|
```bash
|
||||||
./build.sh lib
|
./build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Run examples:**
|
3. **Build and run examples:**
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples
|
./build.sh -e
|
||||||
./examples/simple_keygen
|
./examples/simple_keygen
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ int main() {
|
|||||||
|
|
||||||
**Compile and run:**
|
**Compile and run:**
|
||||||
```bash
|
```bash
|
||||||
gcc example.c -o example ./libnostr_core.a -lm
|
gcc example.c -o example ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||||
./example
|
./example
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -170,27 +170,13 @@ gcc example.c -o example ./libnostr_core.a -lm
|
|||||||
### Build Targets
|
### Build Targets
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build.sh lib # Build static library (default)
|
./build.sh # Build static library (default)
|
||||||
./build.sh examples # Build examples
|
./build.sh --nips=all # Force build with all available NIPs
|
||||||
./build.sh test # Run test suite
|
./build.sh -t # Build all test executables in tests/
|
||||||
./build.sh clean # Clean build artifacts
|
./build.sh -e # Build all examples in examples/
|
||||||
./build.sh install # Install to system
|
./build.sh -t -e # Build library + tests + examples
|
||||||
```
|
./build.sh --nips=46 # Build specifically with NIP-046 support
|
||||||
|
./build.sh --help # Show all options
|
||||||
### Manual Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build static library
|
|
||||||
make
|
|
||||||
|
|
||||||
# Build examples
|
|
||||||
make examples
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
make test-crypto
|
|
||||||
|
|
||||||
# Clean
|
|
||||||
make clean
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
@@ -306,6 +292,7 @@ publish_result_t* synchronous_publish_event_with_progress(const char** relay_url
|
|||||||
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
|
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);
|
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_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);
|
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||||
|
|
||||||
// Subscribe to events (with auto-reconnection)
|
// Subscribe to events (with auto-reconnection)
|
||||||
@@ -314,7 +301,12 @@ nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
|||||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
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);
|
void (*on_eose)(void* user_data), void* user_data, int close_on_eose);
|
||||||
|
|
||||||
// Run event loop (handles reconnection automatically)
|
// 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_run(nostr_relay_pool_t* pool, int timeout_ms);
|
||||||
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
|
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
|
||||||
|
|
||||||
@@ -364,11 +356,14 @@ The library includes comprehensive examples:
|
|||||||
- **`mnemonic_derivation`** - NIP-06 key derivation
|
- **`mnemonic_derivation`** - NIP-06 key derivation
|
||||||
- **`utility_functions`** - General utility demonstrations
|
- **`utility_functions`** - General utility demonstrations
|
||||||
- **`input_detection`** - Input type detection and processing
|
- **`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
|
- **`version_test`** - Library version information
|
||||||
|
|
||||||
Run all examples:
|
Build all examples:
|
||||||
```bash
|
```bash
|
||||||
./build.sh examples
|
./build.sh -e
|
||||||
ls -la examples/
|
ls -la examples/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -377,28 +372,19 @@ ls -la examples/
|
|||||||
The library includes extensive tests:
|
The library includes extensive tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Build all test executables
|
||||||
./build.sh test
|
./build.sh -t
|
||||||
|
|
||||||
# Individual test categories
|
# Run the new NIP-46 test
|
||||||
cd tests && make test
|
./tests/nip46_test
|
||||||
|
|
||||||
# Interactive relay pool testing
|
# Run selected tests
|
||||||
cd tests && ./pool_test
|
./tests/nip44_test
|
||||||
|
./tests/nip42_test
|
||||||
|
./tests/nip17_test
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test Categories:**
|
**Current test binaries live in [`tests/`](tests/) and are generated from `*_test.c` sources.**
|
||||||
- **Core Functionality**: `simple_init_test`, `header_test`
|
|
||||||
- **Cryptography**: `chacha20_test`, `nostr_crypto_test`
|
|
||||||
- **NIP-04 Encryption**: `nip04_test`
|
|
||||||
- **NIP-05 Identifiers**: `nip05_test`
|
|
||||||
- **NIP-11 Relay Info**: `nip11_test`
|
|
||||||
- **NIP-44 Encryption**: `nip44_test`, `nip44_debug_test`
|
|
||||||
- **Key Derivation**: `nostr_test_bip32`
|
|
||||||
- **Relay Communication**: `relay_pool_test`, `sync_test`
|
|
||||||
- **HTTP/WebSocket**: `http_test`, `wss_test`
|
|
||||||
- **Proof of Work**: `test_pow_loop`
|
|
||||||
- **Interactive Pool Testing**: `pool_test` (menu-driven interface with reconnection testing)
|
|
||||||
|
|
||||||
## 🏗️ Integration
|
## 🏗️ Integration
|
||||||
|
|
||||||
@@ -408,7 +394,7 @@ cd tests && ./pool_test
|
|||||||
|
|
||||||
2. **Copy required files to your project:**
|
2. **Copy required files to your project:**
|
||||||
```bash
|
```bash
|
||||||
cp libnostr_core.a /path/to/your/project/
|
cp libnostr_core_x64.a /path/to/your/project/
|
||||||
cp nostr_core/nostr_core.h /path/to/your/project/
|
cp nostr_core/nostr_core.h /path/to/your/project/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -443,7 +429,7 @@ The `libnostr_core.a` library now uses **system dependencies** for all major cry
|
|||||||
|
|
||||||
**Complete linking example:**
|
**Complete linking example:**
|
||||||
```bash
|
```bash
|
||||||
gcc your_app.c ./libnostr_core.a -lssl -lcrypto -lcurl -lsecp256k1 -lm -o your_app
|
gcc your_app.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1 -o your_app
|
||||||
```
|
```
|
||||||
|
|
||||||
**Check system dependencies:**
|
**Check system dependencies:**
|
||||||
@@ -471,8 +457,8 @@ ldd your_app # Shows linked system libraries
|
|||||||
### Build Flags
|
### Build Flags
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable all logging
|
# Enable websocket and PoW debug emission (now callback-based)
|
||||||
make LOGGING_FLAGS="-DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
|
make LOGGING_FLAGS="-DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
|
||||||
|
|
||||||
# Debug build
|
# Debug build
|
||||||
make debug
|
make debug
|
||||||
@@ -481,6 +467,40 @@ make debug
|
|||||||
make arm64
|
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
|
## 🌐 Supported Platforms
|
||||||
|
|
||||||
- **Linux** (x86_64, ARM64)
|
- **Linux** (x86_64, ARM64)
|
||||||
@@ -490,24 +510,24 @@ make arm64
|
|||||||
|
|
||||||
## 📄 Documentation
|
## 📄 Documentation
|
||||||
|
|
||||||
- **[LIBRARY_USAGE.md](LIBRARY_USAGE.md)** - Detailed integration guide
|
- **[POOL_API.md](POOL_API.md)** - Relay pool API notes
|
||||||
- **[EXPORT_GUIDE.md](EXPORT_GUIDE.md)** - Library export instructions
|
- **[nostr_websocket/README.md](nostr_websocket/README.md)** - WebSocket module details
|
||||||
- **[AUTOMATIC_VERSIONING.md](AUTOMATIC_VERSIONING.md)** - Version management
|
- **[nostr_websocket/EXPORT_GUIDE.md](nostr_websocket/EXPORT_GUIDE.md)** - WebSocket export instructions
|
||||||
- **API Reference** - Complete documentation in `nostr_core/nostr_core.h`
|
- **API Reference** - Complete documentation in [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h)
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||||
3. Make your changes and add tests
|
3. Make your changes and add tests
|
||||||
4. Run the test suite: `./build.sh test`
|
4. Build tests and run key suites: `./build.sh -t && ./tests/nip46_test`
|
||||||
5. Commit your changes: `git commit -m 'Add amazing feature'`
|
5. Commit your changes: `git commit -m 'Add amazing feature'`
|
||||||
6. Push to the branch: `git push origin feature/amazing-feature`
|
6. Push to the branch: `git push origin feature/amazing-feature`
|
||||||
7. Open a Pull Request
|
7. Open a Pull Request
|
||||||
|
|
||||||
## 📈 Version History
|
## 📈 Version History
|
||||||
|
|
||||||
Current version: **0.2.1**
|
Current version: **0.4.8**
|
||||||
|
|
||||||
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
|
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
|
||||||
|
|
||||||
@@ -520,10 +540,11 @@ The library uses automatic semantic versioning based on Git tags. Each build inc
|
|||||||
- **Comprehensive Testing**: Extensive test suite and error handling
|
- **Comprehensive Testing**: Extensive test suite and error handling
|
||||||
|
|
||||||
**Version Timeline:**
|
**Version Timeline:**
|
||||||
- `v0.2.x` - Current development releases with enhanced NIP support
|
- `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
|
- `v0.1.x` - Initial development releases
|
||||||
- Focus on core protocol implementation and OpenSSL-based crypto
|
- 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-42, NIP-44, NIP-59 support
|
- 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
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
@@ -553,7 +574,7 @@ The library is small (~500KB) as it links against system libraries (secp256k1, O
|
|||||||
**Linking errors:**
|
**Linking errors:**
|
||||||
Make sure to include the math library:
|
Make sure to include the math library:
|
||||||
```bash
|
```bash
|
||||||
gcc your_code.c ./libnostr_core.a -lm # Note the -lm flag
|
gcc your_code.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting Help
|
### Getting Help
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ if [ "$HELP" = true ]; then
|
|||||||
echo " 021 - nostr: URI scheme"
|
echo " 021 - nostr: URI scheme"
|
||||||
echo " 042 - Authentication of clients to relays"
|
echo " 042 - Authentication of clients to relays"
|
||||||
echo " 044 - Encryption (modern)"
|
echo " 044 - Encryption (modern)"
|
||||||
|
echo " 046 - Remote signing"
|
||||||
echo " 059 - Gift Wrap"
|
echo " 059 - Gift Wrap"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
@@ -194,7 +195,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
|
|||||||
NEEDED_NIPS=""
|
NEEDED_NIPS=""
|
||||||
if [ -n "$FORCE_NIPS" ]; then
|
if [ -n "$FORCE_NIPS" ]; then
|
||||||
if [ "$FORCE_NIPS" = "all" ]; then
|
if [ "$FORCE_NIPS" = "all" ]; then
|
||||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 059"
|
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 046 059"
|
||||||
print_info "Forced: Building all available NIPs"
|
print_info "Forced: Building all available NIPs"
|
||||||
else
|
else
|
||||||
# Convert comma-separated list to space-separated with 3-digit format
|
# Convert comma-separated list to space-separated with 3-digit format
|
||||||
@@ -213,7 +214,7 @@ else
|
|||||||
# Check for nostr_core.h (includes everything)
|
# Check for nostr_core.h (includes everything)
|
||||||
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
|
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
|
||||||
print_info "Found #include \"nostr_core.h\" - building all NIPs"
|
print_info "Found #include \"nostr_core.h\" - building all NIPs"
|
||||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
NEEDED_NIPS="001 004 005 006 011 013 019 042 044 046 059"
|
||||||
elif [ -n "$DETECTED" ]; then
|
elif [ -n "$DETECTED" ]; then
|
||||||
NEEDED_NIPS="$DETECTED"
|
NEEDED_NIPS="$DETECTED"
|
||||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||||
@@ -231,7 +232,7 @@ fi
|
|||||||
|
|
||||||
# If building tests or examples, include all NIPs to ensure compatibility
|
# If building tests or examples, include all NIPs to ensure compatibility
|
||||||
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
|
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
|
||||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 042 044 059"
|
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 042 044 046 059"
|
||||||
print_info "Building tests/examples - including all available NIPs for compatibility"
|
print_info "Building tests/examples - including all available NIPs for compatibility"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -501,6 +502,7 @@ SOURCES="$SOURCES nostr_core/utils.c"
|
|||||||
SOURCES="$SOURCES nostr_core/nostr_common.c"
|
SOURCES="$SOURCES nostr_core/nostr_common.c"
|
||||||
SOURCES="$SOURCES nostr_core/core_relays.c"
|
SOURCES="$SOURCES nostr_core/core_relays.c"
|
||||||
SOURCES="$SOURCES nostr_core/core_relay_pool.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_websocket/nostr_websocket_openssl.c"
|
||||||
SOURCES="$SOURCES nostr_core/request_validator.c"
|
SOURCES="$SOURCES nostr_core/request_validator.c"
|
||||||
|
|
||||||
@@ -522,6 +524,7 @@ for nip in $NEEDED_NIPS; do
|
|||||||
021) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-021(URI)" ;;
|
021) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-021(URI)" ;;
|
||||||
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
|
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
|
||||||
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
|
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)" ;;
|
059) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-059(Gift-Wrap)" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+190
-13
@@ -26,8 +26,8 @@
|
|||||||
// cJSON for JSON handling
|
// cJSON for JSON handling
|
||||||
#include "../cjson/cJSON.h"
|
#include "../cjson/cJSON.h"
|
||||||
|
|
||||||
|
// NIP-42 Authentication
|
||||||
|
#include "nip042.h"
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// RELAY POOL MANAGEMENT
|
// RELAY POOL MANAGEMENT
|
||||||
@@ -105,6 +105,12 @@ typedef struct relay_connection {
|
|||||||
char last_connection_error[512]; // Last error message from relay connection
|
char last_connection_error[512]; // Last error message from relay connection
|
||||||
time_t last_connection_error_time; // When the connection error occurred
|
time_t last_connection_error_time; // When the connection error occurred
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
nostr_relay_stats_t stats;
|
nostr_relay_stats_t stats;
|
||||||
} relay_connection_t;
|
} relay_connection_t;
|
||||||
@@ -165,6 +171,11 @@ struct nostr_relay_pool {
|
|||||||
|
|
||||||
// Pool-wide settings
|
// Pool-wide settings
|
||||||
int default_timeout_ms;
|
int default_timeout_ms;
|
||||||
|
|
||||||
|
// NIP-42 Authentication configuration
|
||||||
|
int nip42_enabled;
|
||||||
|
unsigned char private_key[32];
|
||||||
|
int has_private_key;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to generate unique subscription IDs
|
// Helper function to generate unique subscription IDs
|
||||||
@@ -602,6 +613,38 @@ nostr_relay_pool_t* nostr_relay_pool_create_compat(void) {
|
|||||||
return nostr_relay_pool_create(NULL);
|
return nostr_relay_pool_create(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
|
||||||
|
const unsigned char* private_key,
|
||||||
|
int enable) {
|
||||||
|
if (!pool) {
|
||||||
|
return NOSTR_ERROR_INVALID_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pool->nip42_enabled = enable ? 1 : 0;
|
||||||
|
|
||||||
|
if (private_key) {
|
||||||
|
memcpy(pool->private_key, private_key, sizeof(pool->private_key));
|
||||||
|
pool->has_private_key = 1;
|
||||||
|
} else {
|
||||||
|
memset(pool->private_key, 0, sizeof(pool->private_key));
|
||||||
|
pool->has_private_key = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate to existing relay connections
|
||||||
|
for (int i = 0; i < pool->relay_count; i++) {
|
||||||
|
if (pool->relays[i]) {
|
||||||
|
pool->relays[i]->nip42_enabled = pool->nip42_enabled;
|
||||||
|
if (!pool->nip42_enabled) {
|
||||||
|
pool->relays[i]->auth_state = NOSTR_AUTH_STATE_NONE;
|
||||||
|
memset(pool->relays[i]->auth_challenge, 0, sizeof(pool->relays[i]->auth_challenge));
|
||||||
|
pool->relays[i]->auth_challenge_time = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NOSTR_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) {
|
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||||
if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) {
|
if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) {
|
||||||
return NOSTR_ERROR_INVALID_INPUT;
|
return NOSTR_ERROR_INVALID_INPUT;
|
||||||
@@ -640,6 +683,12 @@ int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url)
|
|||||||
relay->ping_pending = 0;
|
relay->ping_pending = 0;
|
||||||
relay->pending_ping_start_ms = 0.0;
|
relay->pending_ping_start_ms = 0.0;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
// Initialize statistics
|
// Initialize statistics
|
||||||
memset(&relay->stats, 0, sizeof(relay->stats));
|
memset(&relay->stats, 0, sizeof(relay->stats));
|
||||||
relay->stats.connection_uptime_start = time(NULL);
|
relay->stats.connection_uptime_start = time(NULL);
|
||||||
@@ -716,6 +765,11 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pool->has_private_key) {
|
||||||
|
memset(pool->private_key, 0, sizeof(pool->private_key));
|
||||||
|
pool->has_private_key = 0;
|
||||||
|
}
|
||||||
|
|
||||||
free(pool);
|
free(pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1067,8 +1121,36 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
|||||||
// Update relay statistics
|
// Update relay statistics
|
||||||
if (success) {
|
if (success) {
|
||||||
relay->stats.events_published_ok++;
|
relay->stats.events_published_ok++;
|
||||||
|
if (relay->auth_state == NOSTR_AUTH_STATE_AUTHENTICATING) {
|
||||||
|
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATED;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
relay->stats.events_published_failed++;
|
relay->stats.events_published_failed++;
|
||||||
|
|
||||||
|
if (error_message && strstr(error_message, "auth-required") != NULL) {
|
||||||
|
relay->auth_state = NOSTR_AUTH_STATE_REJECTED;
|
||||||
|
|
||||||
|
// If we already have a challenge, immediately attempt re-authentication
|
||||||
|
if (relay->nip42_enabled && pool->has_private_key && relay->auth_challenge[0] != '\0') {
|
||||||
|
cJSON* auth_event = nostr_nip42_create_auth_event(
|
||||||
|
relay->auth_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store error message for legacy API
|
// Store error message for legacy API
|
||||||
if (error_message) {
|
if (error_message) {
|
||||||
strncpy(relay->last_publish_error, error_message,
|
strncpy(relay->last_publish_error, error_message,
|
||||||
@@ -1094,6 +1176,51 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||||
|
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
|
||||||
|
if (relay->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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "NOTICE") == 0) {
|
||||||
|
// Handle NOTICE message: ["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);
|
||||||
|
if (notice && strstr(notice, "auth-required") != NULL) {
|
||||||
|
relay->auth_state = NOSTR_AUTH_STATE_REJECTED;
|
||||||
|
strncpy(relay->last_publish_error, notice, sizeof(relay->last_publish_error) - 1);
|
||||||
|
relay->last_publish_error[sizeof(relay->last_publish_error) - 1] = '\0';
|
||||||
|
relay->last_publish_error_time = time(NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (strcmp(msg_type, "PONG") == 0) {
|
} else if (strcmp(msg_type, "PONG") == 0) {
|
||||||
// Handle PONG response for connection health monitoring
|
// Handle PONG response for connection health monitoring
|
||||||
handle_pong_response(relay);
|
handle_pong_response(relay);
|
||||||
@@ -1165,7 +1292,7 @@ cJSON** nostr_relay_pool_query_sync(
|
|||||||
for (int i = 0; i < connected_count; i++) {
|
for (int i = 0; i < connected_count; i++) {
|
||||||
relay_connection_t* relay = connected_relays[i];
|
relay_connection_t* relay = connected_relays[i];
|
||||||
|
|
||||||
char buffer[8192];
|
char buffer[65536];
|
||||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100);
|
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
buffer[len] = '\0';
|
buffer[len] = '\0';
|
||||||
@@ -1175,29 +1302,30 @@ cJSON** nostr_relay_pool_query_sync(
|
|||||||
// Handle pong response for connection health monitoring
|
// Handle pong response for connection health monitoring
|
||||||
handle_pong_response(relay);
|
handle_pong_response(relay);
|
||||||
} else {
|
} else {
|
||||||
// Process as regular NOSTR message
|
// Parse once: query-sync consumes only its own sub-id messages;
|
||||||
|
// everything else is forwarded to normal pool dispatch.
|
||||||
char* msg_type = NULL;
|
char* msg_type = NULL;
|
||||||
cJSON* parsed = NULL;
|
cJSON* parsed = NULL;
|
||||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||||
|
int handled_for_query = 0;
|
||||||
|
|
||||||
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||||
// Handle EVENT message
|
|
||||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
||||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||||
cJSON* event = cJSON_GetArrayItem(parsed, 2);
|
cJSON* event = cJSON_GetArrayItem(parsed, 2);
|
||||||
|
|
||||||
if (cJSON_IsString(sub_id_json) && event &&
|
if (cJSON_IsString(sub_id_json) && event &&
|
||||||
strcmp(cJSON_GetStringValue(sub_id_json), subscription_id) == 0) {
|
strcmp(cJSON_GetStringValue(sub_id_json), subscription_id) == 0) {
|
||||||
|
handled_for_query = 1;
|
||||||
|
|
||||||
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
|
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
|
||||||
if (event_id_json && cJSON_IsString(event_id_json)) {
|
if (event_id_json && cJSON_IsString(event_id_json)) {
|
||||||
const char* event_id = cJSON_GetStringValue(event_id_json);
|
const char* event_id = cJSON_GetStringValue(event_id_json);
|
||||||
|
|
||||||
// Check for duplicate
|
|
||||||
if (!is_event_seen(pool, event_id)) {
|
if (!is_event_seen(pool, event_id)) {
|
||||||
mark_event_seen(pool, event_id);
|
mark_event_seen(pool, event_id);
|
||||||
relay->stats.events_received++;
|
relay->stats.events_received++;
|
||||||
|
|
||||||
// Add to results array
|
|
||||||
if (*event_count >= events_capacity) {
|
if (*event_count >= events_capacity) {
|
||||||
events_capacity = events_capacity == 0 ? 10 : events_capacity * 2;
|
events_capacity = events_capacity == 0 ? 10 : events_capacity * 2;
|
||||||
events = realloc(events, events_capacity * sizeof(cJSON*));
|
events = realloc(events, events_capacity * sizeof(cJSON*));
|
||||||
@@ -1213,17 +1341,53 @@ cJSON** nostr_relay_pool_query_sync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
|
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
|
||||||
// Handle EOSE message
|
|
||||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||||
if (cJSON_IsString(sub_id_json) &&
|
if (cJSON_IsString(sub_id_json) &&
|
||||||
strcmp(cJSON_GetStringValue(sub_id_json), subscription_id) == 0) {
|
strcmp(cJSON_GetStringValue(sub_id_json), subscription_id) == 0) {
|
||||||
|
handled_for_query = 1;
|
||||||
eose_count++;
|
eose_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (msg_type && strcmp(msg_type, "AUTH") == 0) {
|
||||||
|
if (relay->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)) {
|
||||||
|
handled_for_query = 1;
|
||||||
|
const char* challenge = cJSON_GetStringValue(challenge_json);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (msg_type) free(msg_type);
|
if (msg_type) free(msg_type);
|
||||||
if (parsed) cJSON_Delete(parsed);
|
if (parsed) cJSON_Delete(parsed);
|
||||||
|
|
||||||
|
if (!handled_for_query) {
|
||||||
|
process_relay_message(pool, relay, buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1322,8 +1486,13 @@ int nostr_relay_pool_publish_async(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relay && ensure_relay_connection(relay) == 0) {
|
int relay_connected = 0;
|
||||||
// Send EVENT message
|
if (relay && relay->ws_client) {
|
||||||
|
relay_connected = (nostr_ws_get_state(relay->ws_client) == NOSTR_WS_CONNECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay_connected) {
|
||||||
|
// Send EVENT message only on already-connected relays
|
||||||
if (nostr_relay_send_event(relay->ws_client, event) >= 0) {
|
if (nostr_relay_send_event(relay->ws_client, event) >= 0) {
|
||||||
relay->stats.events_published++;
|
relay->stats.events_published++;
|
||||||
success_count++;
|
success_count++;
|
||||||
@@ -1341,9 +1510,9 @@ int nostr_relay_pool_publish_async(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Connection failed - notify callback immediately if provided
|
// Relay not currently connected - publish is skipped; reconnection is handled by poll loop
|
||||||
if (callback && op) {
|
if (callback && op) {
|
||||||
callback(relay_urls[i], event_id, 0, "Failed to connect to relay", user_data);
|
callback(relay_urls[i], event_id, 0, "Relay not connected; publish skipped", user_data);
|
||||||
|
|
||||||
// Remove this relay from the pending operation
|
// Remove this relay from the pending operation
|
||||||
int remaining = remove_relay_from_publish_operation(op, relay_urls[i]);
|
int remaining = remove_relay_from_publish_operation(op, relay_urls[i]);
|
||||||
@@ -1723,7 +1892,15 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
|
|||||||
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
|
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
|
||||||
|
|
||||||
if (state != NOSTR_WS_CONNECTED) {
|
if (state != NOSTR_WS_CONNECTED) {
|
||||||
|
nostr_pool_relay_status_t prev_status = relay->status;
|
||||||
relay->status = (state == NOSTR_WS_ERROR) ? NOSTR_POOL_RELAY_ERROR : NOSTR_POOL_RELAY_DISCONNECTED;
|
relay->status = (state == NOSTR_WS_ERROR) ? NOSTR_POOL_RELAY_ERROR : NOSTR_POOL_RELAY_DISCONNECTED;
|
||||||
|
|
||||||
|
// Capture transition cause for observability/debugging
|
||||||
|
if (prev_status == NOSTR_POOL_RELAY_CONNECTED || relay->last_connection_error_time == 0) {
|
||||||
|
snprintf(relay->last_connection_error, sizeof(relay->last_connection_error),
|
||||||
|
"Connection state transitioned to %d during poll", (int)state);
|
||||||
|
relay->last_connection_error_time = time(NULL);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1733,7 +1910,7 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
|
|||||||
check_connection_health(relay);
|
check_connection_health(relay);
|
||||||
|
|
||||||
// Process incoming messages
|
// Process incoming messages
|
||||||
char buffer[8192];
|
char buffer[65536];
|
||||||
int timeout_per_relay = timeout_ms / pool->relay_count;
|
int timeout_per_relay = timeout_ms / pool->relay_count;
|
||||||
|
|
||||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, timeout_per_relay);
|
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, timeout_per_relay);
|
||||||
|
|||||||
+23
-18
@@ -6,6 +6,7 @@
|
|||||||
#include "nip001.h"
|
#include "nip001.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "../cjson/cJSON.h"
|
#include "../cjson/cJSON.h"
|
||||||
|
#include "nostr_log.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -175,12 +176,13 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
|||||||
if (attempts % timestamp_update_interval == 0) {
|
if (attempts % timestamp_update_interval == 0) {
|
||||||
current_timestamp = time(NULL);
|
current_timestamp = time(NULL);
|
||||||
#ifdef ENABLE_DEBUG_LOGGING
|
#ifdef ENABLE_DEBUG_LOGGING
|
||||||
FILE* f = fopen("debug.log", "a");
|
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
|
||||||
if (f) {
|
"nip013",
|
||||||
fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n",
|
"PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d",
|
||||||
attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty);
|
attempts,
|
||||||
fclose(f);
|
best_difficulty_this_round,
|
||||||
}
|
best_difficulty_overall,
|
||||||
|
target_difficulty);
|
||||||
#endif
|
#endif
|
||||||
// Reset best difficulty for the new round
|
// Reset best difficulty for the new round
|
||||||
best_difficulty_this_round = 0;
|
best_difficulty_this_round = 0;
|
||||||
@@ -239,19 +241,23 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
|||||||
// Check if we've reached the target
|
// Check if we've reached the target
|
||||||
if (current_difficulty >= target_difficulty) {
|
if (current_difficulty >= target_difficulty) {
|
||||||
#ifdef ENABLE_DEBUG_LOGGING
|
#ifdef ENABLE_DEBUG_LOGGING
|
||||||
FILE* f = fopen("debug.log", "a");
|
nostr_log_emitf(NOSTR_LOG_LEVEL_INFO,
|
||||||
if (f) {
|
"nip013",
|
||||||
fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n",
|
"PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts",
|
||||||
current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1);
|
current_difficulty,
|
||||||
|
target_difficulty,
|
||||||
|
(unsigned long long)nonce,
|
||||||
|
attempts + 1);
|
||||||
|
|
||||||
// Print the final event JSON
|
// Print the final event JSON
|
||||||
char* event_json = cJSON_Print(test_event);
|
char* event_json = cJSON_Print(test_event);
|
||||||
if (event_json) {
|
if (event_json) {
|
||||||
fprintf(f, "Final event: %s\n", event_json);
|
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
|
||||||
|
"nip013",
|
||||||
|
"Final event: %s",
|
||||||
|
event_json);
|
||||||
free(event_json);
|
free(event_json);
|
||||||
}
|
}
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Copy successful result back to input event
|
// Copy successful result back to input event
|
||||||
@@ -267,11 +273,10 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
|||||||
|
|
||||||
#ifdef ENABLE_DEBUG_LOGGING
|
#ifdef ENABLE_DEBUG_LOGGING
|
||||||
// Debug logging - failure
|
// Debug logging - failure
|
||||||
FILE* f = fopen("debug.log", "a");
|
nostr_log_emitf(NOSTR_LOG_LEVEL_WARN,
|
||||||
if (f) {
|
"nip013",
|
||||||
fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts);
|
"PoW FAILED: Mining failed after %d attempts",
|
||||||
fclose(f);
|
max_attempts);
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// If we reach here, we've exceeded max attempts
|
// If we reach here, we've exceeded max attempts
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -52,6 +52,18 @@ const char* nostr_strerror(int error_code) {
|
|||||||
case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format";
|
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_SHORT: return "NIP-42: Challenge too short";
|
||||||
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long";
|
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";
|
||||||
default: return "Unknown error";
|
default: return "Unknown error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,20 @@
|
|||||||
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
|
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
|
||||||
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
|
#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
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
#define NOSTR_PRIVATE_KEY_SIZE 32
|
#define NOSTR_PRIVATE_KEY_SIZE 32
|
||||||
#define NOSTR_PUBLIC_KEY_SIZE 32
|
#define NOSTR_PUBLIC_KEY_SIZE 32
|
||||||
|
|||||||
+14
-2
@@ -2,10 +2,10 @@
|
|||||||
#define NOSTR_CORE_H
|
#define NOSTR_CORE_H
|
||||||
|
|
||||||
// Version information (auto-updated by increment_and_push.sh)
|
// Version information (auto-updated by increment_and_push.sh)
|
||||||
#define VERSION "v0.4.7"
|
#define VERSION "v0.4.13"
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 4
|
#define VERSION_MINOR 4
|
||||||
#define VERSION_PATCH 7
|
#define VERSION_PATCH 13
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* NOSTR Core Library - Complete API Reference
|
* NOSTR Core Library - Complete API Reference
|
||||||
@@ -49,6 +49,13 @@
|
|||||||
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
|
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
|
||||||
* - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages
|
* - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages
|
||||||
*
|
*
|
||||||
|
* NIP-46 REMOTE SIGNING:
|
||||||
|
* - nostr_nip46_parse_bunker_url() -> Parse bunker:// connection tokens
|
||||||
|
* - nostr_nip46_parse_nostrconnect_url() -> Parse nostrconnect:// connection tokens
|
||||||
|
* - nostr_nip46_create_request_event() -> Create encrypted kind 24133 request events
|
||||||
|
* - nostr_nip46_create_response_event() -> Create encrypted kind 24133 response events
|
||||||
|
* - nostr_nip46_signer_handle_request() -> Handle signer-side RPC requests
|
||||||
|
*
|
||||||
* NIP-59 GIFT WRAP:
|
* NIP-59 GIFT WRAP:
|
||||||
* - nostr_nip59_create_rumor() -> Create unsigned event (rumor)
|
* - nostr_nip59_create_rumor() -> Create unsigned event (rumor)
|
||||||
* - nostr_nip59_create_seal() -> Seal rumor with sender's key (kind 13)
|
* - nostr_nip59_create_seal() -> Seal rumor with sender's key (kind 13)
|
||||||
@@ -180,11 +187,15 @@ extern "C" {
|
|||||||
#include "nip021.h" // nostr: URI scheme
|
#include "nip021.h" // nostr: URI scheme
|
||||||
#include "nip042.h" // Authentication of clients to relays
|
#include "nip042.h" // Authentication of clients to relays
|
||||||
#include "nip044.h" // Encryption (modern)
|
#include "nip044.h" // Encryption (modern)
|
||||||
|
#include "nip046.h" // Remote signing
|
||||||
#include "nip059.h" // Gift Wrap
|
#include "nip059.h" // Gift Wrap
|
||||||
|
|
||||||
// Authentication and request validation system
|
// Authentication and request validation system
|
||||||
#include "request_validator.h" // Request validation and authentication rules
|
#include "request_validator.h" // Request validation and authentication rules
|
||||||
|
|
||||||
|
// Logging callback API
|
||||||
|
#include "nostr_log.h"
|
||||||
|
|
||||||
// Relay pool types and functions
|
// Relay pool types and functions
|
||||||
typedef enum {
|
typedef enum {
|
||||||
NOSTR_POOL_RELAY_DISCONNECTED = 0,
|
NOSTR_POOL_RELAY_DISCONNECTED = 0,
|
||||||
@@ -241,6 +252,7 @@ nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* confi
|
|||||||
nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void);
|
nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void);
|
||||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||||
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||||
|
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable);
|
||||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||||
|
|
||||||
// Subscription management
|
// Subscription management
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#define _GNU_SOURCE
|
#define _GNU_SOURCE
|
||||||
#include "nostr_websocket_tls.h"
|
#include "nostr_websocket_tls.h"
|
||||||
|
#include "../nostr_core/nostr_log.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -24,14 +25,6 @@
|
|||||||
#define MAX_HEADER_SIZE 4096
|
#define MAX_HEADER_SIZE 4096
|
||||||
#define MAX_FRAME_SIZE 65536
|
#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
|
// Transport layer abstraction
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int (*connect)(void* ctx, const char* host, int port);
|
int (*connect)(void* ctx, const char* host, int port);
|
||||||
@@ -454,6 +447,13 @@ static int tcp_connect(void* ctx, const char* host, int port) {
|
|||||||
return -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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,23 +580,30 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if SSL has pending data first
|
// Always use select() to ensure proper blocking behavior
|
||||||
if (SSL_pending(tls->ssl) == 0 && timeout_ms > 0) {
|
// If SSL has pending data, use zero timeout to return immediately
|
||||||
// Only use select() if no data is pending in SSL buffers
|
// Otherwise use the full timeout to block until data arrives
|
||||||
|
if (timeout_ms > 0) {
|
||||||
fd_set readfds;
|
fd_set readfds;
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
|
|
||||||
FD_ZERO(&readfds);
|
FD_ZERO(&readfds);
|
||||||
FD_SET(tls->socket_fd, &readfds);
|
FD_SET(tls->socket_fd, &readfds);
|
||||||
|
|
||||||
|
// 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_sec = timeout_ms / 1000;
|
||||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
int result = select(tls->socket_fd + 1, &readfds, NULL, NULL, &tv);
|
int result = select(tls->socket_fd + 1, &readfds, NULL, NULL, &tv);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (result == 0) {
|
} else if (result == 0 && SSL_pending(tls->ssl) == 0) {
|
||||||
return -1; // Timeout
|
return -1; // Timeout with no pending data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,10 +860,15 @@ static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const ch
|
|||||||
frame_len += payload_len;
|
frame_len += payload_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log outgoing message to debug.log
|
// Emit outgoing message to consumer callback logger
|
||||||
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
|
#if defined(ENABLE_WEBSOCKET_LOGGING)
|
||||||
if (opcode == WS_OPCODE_TEXT && payload && payload_len > 0) {
|
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
|
#endif
|
||||||
|
|
||||||
@@ -901,11 +913,6 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
|||||||
header_len = 10;
|
header_len = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check payload length
|
|
||||||
if (len > *payload_len) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read mask (if present)
|
// Read mask (if present)
|
||||||
uint32_t mask = 0;
|
uint32_t mask = 0;
|
||||||
if (masked) {
|
if (masked) {
|
||||||
@@ -919,6 +926,21 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
|||||||
header_len += 4;
|
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
|
// Read payload
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
size_t received = 0;
|
size_t received = 0;
|
||||||
@@ -936,14 +958,19 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
|||||||
ws_mask_payload(payload, len, mask);
|
ws_mask_payload(payload, len, mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log incoming text messages to debug.log
|
// Emit incoming text messages to consumer callback logger
|
||||||
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
|
#if defined(ENABLE_WEBSOCKET_LOGGING)
|
||||||
if (*opcode == WS_OPCODE_TEXT && len > 0) {
|
if (*opcode == WS_OPCODE_TEXT && len > 0) {
|
||||||
// Null terminate for logging
|
// Null terminate for logging
|
||||||
char temp_payload[len + 1];
|
char temp_payload[len + 1];
|
||||||
memcpy(temp_payload, payload, len);
|
memcpy(temp_payload, payload, len);
|
||||||
temp_payload[len] = '\0';
|
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
|
#endif
|
||||||
}
|
}
|
||||||
@@ -974,44 +1001,3 @@ static uint32_t ws_generate_mask(void) {
|
|||||||
return ((uint32_t)rand() << 16) | ((uint32_t)rand() & 0xFFFF);
|
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
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,309 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#include "../nostr_core/nostr_core.h"
|
||||||
|
#include "../cjson/cJSON.h"
|
||||||
|
#include "../nostr_websocket/nostr_websocket_tls.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int g_callback_count = 0;
|
||||||
|
static int g_publish_ok = 0;
|
||||||
|
static int g_publish_fail = 0;
|
||||||
|
static int g_auth_required_seen = 0;
|
||||||
|
|
||||||
|
static const char* relay_status_str(nostr_pool_relay_status_t status) {
|
||||||
|
switch (status) {
|
||||||
|
case NOSTR_POOL_RELAY_DISCONNECTED: return "DISCONNECTED";
|
||||||
|
case NOSTR_POOL_RELAY_CONNECTING: return "CONNECTING";
|
||||||
|
case NOSTR_POOL_RELAY_CONNECTED: return "CONNECTED";
|
||||||
|
case NOSTR_POOL_RELAY_ERROR: return "ERROR";
|
||||||
|
default: return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static double now_ms(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
|
||||||
|
return (double)time(NULL) * 1000.0;
|
||||||
|
}
|
||||||
|
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void publish_callback(const char* relay_url,
|
||||||
|
const char* event_id,
|
||||||
|
int success,
|
||||||
|
const char* message,
|
||||||
|
void* user_data) {
|
||||||
|
(void)user_data;
|
||||||
|
|
||||||
|
g_callback_count++;
|
||||||
|
if (success) {
|
||||||
|
g_publish_ok++;
|
||||||
|
} else {
|
||||||
|
g_publish_fail++;
|
||||||
|
if (message && strstr(message, "auth-required") != NULL) {
|
||||||
|
g_auth_required_seen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[POOL CALLBACK %d] relay=%s event_id=%s success=%d", g_callback_count,
|
||||||
|
relay_url ? relay_url : "(null)", event_id ? event_id : "(null)", success);
|
||||||
|
if (message) {
|
||||||
|
printf(" message=\"%s\"", message);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* create_kind4_event(const unsigned char* private_key, int sequence) {
|
||||||
|
if (!private_key) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char content[256];
|
||||||
|
snprintf(content, sizeof(content), "pool-auth-test message #%d at %ld", sequence, (long)time(NULL));
|
||||||
|
|
||||||
|
cJSON* tags = cJSON_CreateArray();
|
||||||
|
if (!tags) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("0000000000000000000000000000000000000000000000000000000000000000"));
|
||||||
|
cJSON_AddItemToArray(tags, p_tag);
|
||||||
|
|
||||||
|
cJSON* event = nostr_create_and_sign_event(4, content, tags, private_key, 0);
|
||||||
|
cJSON_Delete(tags);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run_relay_auth_probe(const char* relay_url) {
|
||||||
|
printf("\n=== Relay AUTH Probe (raw responses) ===\n");
|
||||||
|
|
||||||
|
nostr_ws_client_t* probe = nostr_ws_connect(relay_url);
|
||||||
|
if (!probe) {
|
||||||
|
printf("[AUTH PROBE] connect failed for %s\n", relay_url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int auth_seen = 0;
|
||||||
|
int ok_seen = 0;
|
||||||
|
int notice_seen = 0;
|
||||||
|
|
||||||
|
double start = now_ms();
|
||||||
|
while ((now_ms() - start) < 2500.0) {
|
||||||
|
char buffer[8192];
|
||||||
|
int len = nostr_ws_receive(probe, buffer, sizeof(buffer) - 1, 150);
|
||||||
|
if (len <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[len] = '\0';
|
||||||
|
printf("[AUTH PROBE RAW] %s\n", buffer);
|
||||||
|
|
||||||
|
char* msg_type = NULL;
|
||||||
|
cJSON* parsed = NULL;
|
||||||
|
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||||
|
if (msg_type) {
|
||||||
|
printf("[AUTH PROBE PARSED] type=%s\n", msg_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg_type && strcmp(msg_type, "AUTH") == 0 && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||||
|
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||||
|
if (cJSON_IsString(challenge_json)) {
|
||||||
|
printf("[AUTH PROBE] challenge=%s\n", cJSON_GetStringValue(challenge_json));
|
||||||
|
}
|
||||||
|
auth_seen++;
|
||||||
|
} else if (msg_type && strcmp(msg_type, "OK") == 0) {
|
||||||
|
ok_seen++;
|
||||||
|
} else if (msg_type && strcmp(msg_type, "NOTICE") == 0) {
|
||||||
|
notice_seen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg_type) free(msg_type);
|
||||||
|
if (parsed) cJSON_Delete(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[AUTH PROBE] summary: AUTH=%d OK=%d NOTICE=%d\n", auth_seen, ok_seen, notice_seen);
|
||||||
|
nostr_ws_close(probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
printf("=== NIP-42 Relay Pool Publish Test (kind-4 over 5s) ===\n");
|
||||||
|
|
||||||
|
if (nostr_init() != NOSTR_SUCCESS) {
|
||||||
|
printf("FAILED: nostr_init() failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* relay_url = "ws://127.0.0.1:7777";
|
||||||
|
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||||
|
|
||||||
|
unsigned char private_key[32];
|
||||||
|
memset(private_key, 0, sizeof(private_key));
|
||||||
|
if (nostr_hex_to_bytes(private_key_hex, private_key, sizeof(private_key)) != NOSTR_SUCCESS) {
|
||||||
|
printf("FAILED: unable to decode private key hex\n");
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char public_key[32];
|
||||||
|
char pubkey_hex[65];
|
||||||
|
if (nostr_ec_public_key_from_private_key(private_key, public_key) != NOSTR_SUCCESS) {
|
||||||
|
printf("FAILED: unable to derive public key\n");
|
||||||
|
memset(private_key, 0, sizeof(private_key));
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
|
||||||
|
printf("Using pubkey: %s\n", pubkey_hex);
|
||||||
|
|
||||||
|
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
|
||||||
|
if (!pool) {
|
||||||
|
printf("FAILED: unable to create relay pool\n");
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nostr_relay_pool_set_auth(pool, private_key, 1) != NOSTR_SUCCESS) {
|
||||||
|
printf("FAILED: unable to configure relay pool authentication\n");
|
||||||
|
nostr_relay_pool_destroy(pool);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nostr_relay_pool_add_relay(pool, relay_url) != NOSTR_SUCCESS) {
|
||||||
|
printf("FAILED: unable to add relay %s\n", relay_url);
|
||||||
|
nostr_relay_pool_destroy(pool);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
run_relay_auth_probe(relay_url);
|
||||||
|
|
||||||
|
const char* relays[] = { relay_url };
|
||||||
|
int sent_attempts = 0;
|
||||||
|
|
||||||
|
const double test_duration_ms = 5000.0;
|
||||||
|
const double publish_interval_ms = 650.0;
|
||||||
|
double start = now_ms();
|
||||||
|
double next_publish = start;
|
||||||
|
int sequence = 1;
|
||||||
|
|
||||||
|
nostr_pool_relay_status_t last_status = NOSTR_POOL_RELAY_DISCONNECTED;
|
||||||
|
char last_error_snapshot[512] = {0};
|
||||||
|
|
||||||
|
while ((now_ms() - start) < test_duration_ms) {
|
||||||
|
nostr_relay_pool_poll(pool, 100);
|
||||||
|
|
||||||
|
nostr_pool_relay_status_t current_status = nostr_relay_pool_get_relay_status(pool, relay_url);
|
||||||
|
if (current_status != last_status) {
|
||||||
|
printf("[POOL STATUS] %s -> %s\n", relay_status_str(last_status), relay_status_str(current_status));
|
||||||
|
last_status = current_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* last_err_live = nostr_relay_pool_get_relay_last_publish_error(pool, relay_url);
|
||||||
|
if (last_err_live && strcmp(last_err_live, last_error_snapshot) != 0) {
|
||||||
|
strncpy(last_error_snapshot, last_err_live, sizeof(last_error_snapshot) - 1);
|
||||||
|
last_error_snapshot[sizeof(last_error_snapshot) - 1] = '\0';
|
||||||
|
printf("[POOL ERROR] %s\n", last_error_snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
double t = now_ms();
|
||||||
|
if (t >= next_publish) {
|
||||||
|
cJSON* event = create_kind4_event(private_key, sequence++);
|
||||||
|
if (!event) {
|
||||||
|
printf("WARN: failed to create signed event, skipping publish\n");
|
||||||
|
next_publish += publish_interval_ms;
|
||||||
|
usleep(100000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sent = nostr_relay_pool_publish_async(pool, relays, 1, event, publish_callback, NULL);
|
||||||
|
if (sent > 0) {
|
||||||
|
sent_attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[PUBLISH] attempt=%d sent=%d\n", sequence - 1, sent);
|
||||||
|
|
||||||
|
cJSON_Delete(event);
|
||||||
|
next_publish += publish_interval_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain callbacks/messages a bit after last publish
|
||||||
|
double drain_start = now_ms();
|
||||||
|
while ((now_ms() - drain_start) < 1500.0) {
|
||||||
|
nostr_relay_pool_poll(pool, 100);
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* last_err = nostr_relay_pool_get_relay_last_publish_error(pool, relay_url);
|
||||||
|
|
||||||
|
cJSON* auth_filter = cJSON_CreateObject();
|
||||||
|
cJSON* auth_kinds = cJSON_CreateArray();
|
||||||
|
cJSON* auth_authors = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(auth_kinds, cJSON_CreateNumber(22242));
|
||||||
|
cJSON_AddItemToArray(auth_authors, cJSON_CreateString(pubkey_hex));
|
||||||
|
cJSON_AddItemToObject(auth_filter, "kinds", auth_kinds);
|
||||||
|
cJSON_AddItemToObject(auth_filter, "authors", auth_authors);
|
||||||
|
cJSON_AddItemToObject(auth_filter, "limit", cJSON_CreateNumber(20));
|
||||||
|
|
||||||
|
int auth_event_count = 0;
|
||||||
|
cJSON** auth_events = nostr_relay_pool_query_sync(pool, relays, 1, auth_filter, &auth_event_count, 2500);
|
||||||
|
cJSON_Delete(auth_filter);
|
||||||
|
|
||||||
|
printf("\n=== AUTH Events Seen On Relay (kind 22242) ===\n");
|
||||||
|
printf("count=%d\n", auth_event_count);
|
||||||
|
for (int i = 0; i < auth_event_count; i++) {
|
||||||
|
cJSON* id = cJSON_GetObjectItem(auth_events[i], "id");
|
||||||
|
cJSON* created_at = cJSON_GetObjectItem(auth_events[i], "created_at");
|
||||||
|
printf("[AUTH EVENT %d] id=%s created_at=%lld\n",
|
||||||
|
i + 1,
|
||||||
|
(id && cJSON_IsString(id)) ? cJSON_GetStringValue(id) : "(no-id)",
|
||||||
|
(long long)((created_at && cJSON_IsNumber(created_at)) ? cJSON_GetNumberValue(created_at) : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n=== Summary ===\n");
|
||||||
|
printf("Relay: %s\n", relay_url);
|
||||||
|
printf("Publish attempts sent: %d\n", sent_attempts);
|
||||||
|
printf("Callbacks: %d\n", g_callback_count);
|
||||||
|
printf("Accepted: %d\n", g_publish_ok);
|
||||||
|
printf("Rejected/Failed: %d\n", g_publish_fail);
|
||||||
|
printf("auth-required seen in callbacks: %d\n", g_auth_required_seen);
|
||||||
|
printf("Last relay publish error: %s\n", last_err ? last_err : "(none)");
|
||||||
|
|
||||||
|
if (auth_events) {
|
||||||
|
for (int i = 0; i < auth_event_count; i++) {
|
||||||
|
if (auth_events[i]) {
|
||||||
|
cJSON_Delete(auth_events[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(auth_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
nostr_relay_pool_destroy(pool);
|
||||||
|
memset(private_key, 0, sizeof(private_key));
|
||||||
|
memset(public_key, 0, sizeof(public_key));
|
||||||
|
nostr_cleanup();
|
||||||
|
|
||||||
|
// Test is considered successful if the loop executed and we attempted publishes.
|
||||||
|
// Auth behavior is diagnosed by callback details and summary output.
|
||||||
|
if (sent_attempts <= 0) {
|
||||||
|
printf("RESULT: FAIL (no publish attempts were sent)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("RESULT: PASS (publish attempts sent; inspect auth behavior in logs above)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
+13
-13
@@ -46,7 +46,7 @@ static nip44_test_vector_t test_vectors[] = {
|
|||||||
NULL
|
NULL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"1MB payload test",
|
"64KB payload test",
|
||||||
"91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", // Same keys as basic test
|
"91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", // Same keys as basic test
|
||||||
"96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220",
|
"96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220",
|
||||||
NULL, // Will be generated dynamically
|
NULL, // Will be generated dynamically
|
||||||
@@ -99,12 +99,12 @@ static int test_nip44_round_trip(const nip44_test_vector_t* tv) {
|
|||||||
|
|
||||||
// Special handling for large payload tests
|
// Special handling for large payload tests
|
||||||
char* test_plaintext;
|
char* test_plaintext;
|
||||||
if (strcmp(tv->name, "1MB payload test") == 0) {
|
if (strcmp(tv->name, "64KB payload test") == 0) {
|
||||||
// Generate exactly 1MB (1,048,576 bytes) of predictable content
|
// Generate exactly 64KB (65,535 bytes) of predictable content - max NIP-44 size
|
||||||
const size_t payload_size = 1048576;
|
const size_t payload_size = 65535;
|
||||||
test_plaintext = malloc(payload_size + 1);
|
test_plaintext = malloc(payload_size + 1);
|
||||||
if (!test_plaintext) {
|
if (!test_plaintext) {
|
||||||
printf(" FAIL: Memory allocation failed for 1MB test payload\n");
|
printf(" FAIL: Memory allocation failed for 64KB test payload\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ static int test_nip44_round_trip(const nip44_test_vector_t* tv) {
|
|||||||
}
|
}
|
||||||
test_plaintext[payload_size] = '\0';
|
test_plaintext[payload_size] = '\0';
|
||||||
|
|
||||||
printf(" Generated 1MB test payload (%zu bytes)\n", payload_size);
|
printf(" Generated 64KB test payload (%zu bytes)\n", payload_size);
|
||||||
printf(" Pattern: \"%s\" repeated\n", pattern);
|
printf(" Pattern: \"%s\" repeated\n", pattern);
|
||||||
printf(" First 64 chars: \"%.64s...\"\n", test_plaintext);
|
printf(" First 64 chars: \"%.64s...\"\n", test_plaintext);
|
||||||
printf(" Last 64 chars: \"...%.64s\"\n", test_plaintext + payload_size - 64);
|
printf(" Last 64 chars: \"...%.64s\"\n", test_plaintext + payload_size - 64);
|
||||||
@@ -158,10 +158,10 @@ static int test_nip44_round_trip(const nip44_test_vector_t* tv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test decryption - use recipient private key + sender public key
|
// Test decryption - use recipient private key + sender public key
|
||||||
char* decrypted = malloc(1048576 + 1); // 1MB + 1 for null terminator
|
char* decrypted = malloc(65536 + 1); // 64KB + 1 for null terminator
|
||||||
if (!decrypted) {
|
if (!decrypted) {
|
||||||
printf(" FAIL: Memory allocation failed for decrypted buffer\n");
|
printf(" FAIL: Memory allocation failed for decrypted buffer\n");
|
||||||
if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext);
|
if (strcmp(tv->name, "64KB payload test") == 0) free(test_plaintext);
|
||||||
free(encrypted);
|
free(encrypted);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ static int test_nip44_round_trip(const nip44_test_vector_t* tv) {
|
|||||||
sender_public_key,
|
sender_public_key,
|
||||||
encrypted,
|
encrypted,
|
||||||
decrypted,
|
decrypted,
|
||||||
1048576 + 1
|
65536 + 1
|
||||||
);
|
);
|
||||||
|
|
||||||
if (decrypt_result != NOSTR_SUCCESS) {
|
if (decrypt_result != NOSTR_SUCCESS) {
|
||||||
@@ -192,17 +192,17 @@ static int test_nip44_round_trip(const nip44_test_vector_t* tv) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(tv->name, "1MB payload test") == 0) {
|
if (strcmp(tv->name, "64KB payload test") == 0) {
|
||||||
printf(" ✅ 1MB payload round-trip: PASS\n");
|
printf(" ✅ 64KB payload round-trip: PASS\n");
|
||||||
printf(" ✅ Content verification: All %zu bytes match perfectly!\n", strlen(test_plaintext));
|
printf(" ✅ Content verification: All %zu bytes match perfectly!\n", strlen(test_plaintext));
|
||||||
printf(" Encrypted length: %zu bytes\n", strlen(encrypted));
|
printf(" Encrypted length: %zu bytes\n", strlen(encrypted));
|
||||||
printf(" 🎉 1MB NIP-44 STRESS TEST COMPLETED SUCCESSFULLY! 🎉\n");
|
printf(" 🎉 64KB NIP-44 STRESS TEST COMPLETED SUCCESSFULLY! 🎉\n");
|
||||||
} else {
|
} else {
|
||||||
printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", test_plaintext, decrypted);
|
printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", test_plaintext, decrypted);
|
||||||
printf(" Encrypted output: %s\n", encrypted);
|
printf(" Encrypted output: %s\n", encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext);
|
if (strcmp(tv->name, "64KB payload test") == 0) free(test_plaintext);
|
||||||
free(encrypted);
|
free(encrypted);
|
||||||
free(decrypted);
|
free(decrypted);
|
||||||
|
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,247 @@
|
|||||||
|
/*
|
||||||
|
* NIP-46 Remote Signing Test Suite
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "../nostr_core/nostr_core.h"
|
||||||
|
|
||||||
|
static int tests_run = 0;
|
||||||
|
static int tests_passed = 0;
|
||||||
|
|
||||||
|
static void expect_int(const char* name, int expected, int actual) {
|
||||||
|
tests_run++;
|
||||||
|
if (expected == actual) {
|
||||||
|
tests_passed++;
|
||||||
|
printf("✅ %s (expected=%d actual=%d)\n", name, expected, actual);
|
||||||
|
} else {
|
||||||
|
printf("❌ %s (expected=%d actual=%d)\n", name, expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expect_true(const char* name, int cond) {
|
||||||
|
tests_run++;
|
||||||
|
if (cond) {
|
||||||
|
tests_passed++;
|
||||||
|
printf("✅ %s\n", name);
|
||||||
|
} else {
|
||||||
|
printf("❌ %s\n", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int hex_to_bytes32(const char* hex, unsigned char out[32]) {
|
||||||
|
return nostr_hex_to_bytes(hex, out, 32) == 0 ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_url_parsing_and_generation(void) {
|
||||||
|
printf("\n=== test_url_parsing_and_generation ===\n");
|
||||||
|
|
||||||
|
const char* bunker = "bunker://fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52?relay=wss%3A%2F%2Frelay1.example.com&relay=wss%3A%2F%2Frelay2.example.com&secret=s3cr3t";
|
||||||
|
nostr_nip46_bunker_url_t bu;
|
||||||
|
int rc = nostr_nip46_parse_bunker_url(bunker, &bu);
|
||||||
|
expect_int("parse bunker url", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("bunker pubkey parsed", strcmp(bu.remote_signer_pubkey, "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52") == 0);
|
||||||
|
expect_int("bunker relay count", 2, bu.relay_count);
|
||||||
|
expect_true("bunker relay[0] decoded", strcmp(bu.relays[0], "wss://relay1.example.com") == 0);
|
||||||
|
expect_true("bunker secret parsed", strcmp(bu.secret, "s3cr3t") == 0);
|
||||||
|
|
||||||
|
char bunker_roundtrip[2048];
|
||||||
|
rc = nostr_nip46_create_bunker_url(&bu, bunker_roundtrip, sizeof(bunker_roundtrip));
|
||||||
|
expect_int("create bunker url", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("bunker roundtrip has scheme", strstr(bunker_roundtrip, "bunker://") == bunker_roundtrip);
|
||||||
|
|
||||||
|
const char* nc = "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay1.example.com&secret=0s8j2djs&perms=nip44_encrypt%2Csign_event%3A1&name=My+Client&url=https%3A%2F%2Fclient.example.com";
|
||||||
|
nostr_nip46_nostrconnect_url_t nu;
|
||||||
|
rc = nostr_nip46_parse_nostrconnect_url(nc, &nu);
|
||||||
|
expect_int("parse nostrconnect url", NOSTR_SUCCESS, rc);
|
||||||
|
expect_int("nostrconnect relay count", 1, nu.relay_count);
|
||||||
|
expect_true("nostrconnect secret parsed", strcmp(nu.secret, "0s8j2djs") == 0);
|
||||||
|
expect_true("nostrconnect name decoded (+ to space)", strcmp(nu.name, "My Client") == 0);
|
||||||
|
|
||||||
|
char nc_roundtrip[2048];
|
||||||
|
rc = nostr_nip46_create_nostrconnect_url(&nu, nc_roundtrip, sizeof(nc_roundtrip));
|
||||||
|
expect_int("create nostrconnect url", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("nostrconnect roundtrip has scheme", strstr(nc_roundtrip, "nostrconnect://") == nc_roundtrip);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_request_response_roundtrip(void) {
|
||||||
|
printf("\n=== test_request_response_roundtrip ===\n");
|
||||||
|
|
||||||
|
char id[65];
|
||||||
|
int rc = nostr_nip46_generate_request_id(id, sizeof(id));
|
||||||
|
expect_int("generate request id", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("request id hex length", strlen(id) == 32);
|
||||||
|
|
||||||
|
const char* params[] = {"abc", "def"};
|
||||||
|
nostr_nip46_request_t req;
|
||||||
|
rc = nostr_nip46_create_request(id, NOSTR_NIP46_METHOD_PING, params, 2, &req);
|
||||||
|
expect_int("create request", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
char* req_json = NULL;
|
||||||
|
rc = nostr_nip46_request_to_json(&req, &req_json);
|
||||||
|
expect_int("request to json", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
nostr_nip46_request_t parsed_req;
|
||||||
|
rc = nostr_nip46_parse_request(req_json, &parsed_req);
|
||||||
|
expect_int("parse request", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("parsed request id matches", strcmp(parsed_req.id, id) == 0);
|
||||||
|
expect_true("parsed request method string", strcmp(parsed_req.method_str, "ping") == 0);
|
||||||
|
expect_int("parsed request param count", 2, parsed_req.param_count);
|
||||||
|
|
||||||
|
free(req_json);
|
||||||
|
nostr_nip46_free_request(&req);
|
||||||
|
nostr_nip46_free_request(&parsed_req);
|
||||||
|
|
||||||
|
nostr_nip46_response_t resp;
|
||||||
|
rc = nostr_nip46_create_response(id, "pong", NULL, &resp);
|
||||||
|
expect_int("create response", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
char* resp_json = NULL;
|
||||||
|
rc = nostr_nip46_response_to_json(&resp, &resp_json);
|
||||||
|
expect_int("response to json", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
nostr_nip46_response_t parsed_resp;
|
||||||
|
rc = nostr_nip46_parse_response(resp_json, &parsed_resp);
|
||||||
|
expect_int("parse response", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("parsed response id matches", strcmp(parsed_resp.id, id) == 0);
|
||||||
|
expect_true("parsed response result matches", strcmp(parsed_resp.result, "pong") == 0);
|
||||||
|
|
||||||
|
free(resp_json);
|
||||||
|
nostr_nip46_free_response(&resp);
|
||||||
|
nostr_nip46_free_response(&parsed_resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_event_encryption_flow(void) {
|
||||||
|
printf("\n=== test_event_encryption_flow ===\n");
|
||||||
|
|
||||||
|
const char* client_sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||||
|
const char* signer_sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
|
||||||
|
|
||||||
|
unsigned char client_sk[32], signer_sk[32];
|
||||||
|
unsigned char signer_pk[32];
|
||||||
|
int rc = hex_to_bytes32(client_sk_hex, client_sk);
|
||||||
|
expect_int("client sk parse", 0, rc);
|
||||||
|
rc = hex_to_bytes32(signer_sk_hex, signer_sk);
|
||||||
|
expect_int("signer sk parse", 0, rc);
|
||||||
|
rc = nostr_ec_public_key_from_private_key(signer_sk, signer_pk);
|
||||||
|
expect_int("derive signer public key", 0, rc);
|
||||||
|
|
||||||
|
const char* params[] = {"hello"};
|
||||||
|
nostr_nip46_request_t req;
|
||||||
|
rc = nostr_nip46_create_request("abc123", NOSTR_NIP46_METHOD_PING, params, 1, &req);
|
||||||
|
expect_int("create ping request", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
cJSON* evt = nostr_nip46_create_request_event(&req, client_sk, signer_pk, 0);
|
||||||
|
expect_true("create encrypted request event", evt != NULL);
|
||||||
|
|
||||||
|
char decrypted[65536];
|
||||||
|
rc = nostr_nip46_decrypt_event(evt, signer_sk, decrypted, sizeof(decrypted));
|
||||||
|
expect_int("decrypt request event", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("decrypted payload has method ping", strstr(decrypted, "\"method\":\"ping\"") != NULL);
|
||||||
|
|
||||||
|
cJSON_Delete(evt);
|
||||||
|
nostr_nip46_free_request(&req);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_signer_handle_request(void) {
|
||||||
|
printf("\n=== test_signer_handle_request ===\n");
|
||||||
|
|
||||||
|
const char* signer_sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
|
||||||
|
const char* user_sk_hex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||||
|
unsigned char signer_sk[32], user_sk[32];
|
||||||
|
|
||||||
|
int rc = hex_to_bytes32(signer_sk_hex, signer_sk);
|
||||||
|
expect_int("parse signer sk", 0, rc);
|
||||||
|
rc = hex_to_bytes32(user_sk_hex, user_sk);
|
||||||
|
expect_int("parse user sk", 0, rc);
|
||||||
|
|
||||||
|
const char* relays[] = {"wss://relay.example.com"};
|
||||||
|
nostr_nip46_signer_session_t ss;
|
||||||
|
rc = nostr_nip46_signer_session_init(&ss, signer_sk, user_sk, relays, 1);
|
||||||
|
expect_int("signer session init", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
const char* connect_params[] = { ss.signer_pubkey_hex, "secret123" };
|
||||||
|
nostr_nip46_request_t connect_req;
|
||||||
|
rc = nostr_nip46_create_request("req-connect", NOSTR_NIP46_METHOD_CONNECT, connect_params, 2, &connect_req);
|
||||||
|
expect_int("build connect request", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
nostr_nip46_response_t connect_resp;
|
||||||
|
rc = nostr_nip46_signer_handle_request(&ss, &connect_req, &connect_resp);
|
||||||
|
expect_int("handle connect request", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("connect returns provided secret", connect_resp.result && strcmp(connect_resp.result, "secret123") == 0);
|
||||||
|
nostr_nip46_free_request(&connect_req);
|
||||||
|
nostr_nip46_free_response(&connect_resp);
|
||||||
|
|
||||||
|
nostr_nip46_request_t ping_req;
|
||||||
|
rc = nostr_nip46_create_request("req-ping", NOSTR_NIP46_METHOD_PING, NULL, 0, &ping_req);
|
||||||
|
expect_int("build ping request", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
nostr_nip46_response_t ping_resp;
|
||||||
|
rc = nostr_nip46_signer_handle_request(&ss, &ping_req, &ping_resp);
|
||||||
|
expect_int("handle ping request", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("ping response is pong", ping_resp.result && strcmp(ping_resp.result, "pong") == 0);
|
||||||
|
nostr_nip46_free_request(&ping_req);
|
||||||
|
nostr_nip46_free_response(&ping_resp);
|
||||||
|
|
||||||
|
nostr_nip46_request_t gpk_req;
|
||||||
|
rc = nostr_nip46_create_request("req-gpk", NOSTR_NIP46_METHOD_GET_PUBLIC_KEY, NULL, 0, &gpk_req);
|
||||||
|
expect_int("build get_public_key request", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
nostr_nip46_response_t gpk_resp;
|
||||||
|
rc = nostr_nip46_signer_handle_request(&ss, &gpk_req, &gpk_resp);
|
||||||
|
expect_int("handle get_public_key request", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("get_public_key returns hex", gpk_resp.result && strlen(gpk_resp.result) == 64);
|
||||||
|
nostr_nip46_free_request(&gpk_req);
|
||||||
|
nostr_nip46_free_response(&gpk_resp);
|
||||||
|
|
||||||
|
cJSON* tags = cJSON_CreateArray();
|
||||||
|
cJSON* unsigned_event = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(unsigned_event, "kind", 1);
|
||||||
|
cJSON_AddStringToObject(unsigned_event, "content", "hello signer");
|
||||||
|
cJSON_AddItemToObject(unsigned_event, "tags", tags);
|
||||||
|
cJSON_AddNumberToObject(unsigned_event, "created_at", (double)time(NULL));
|
||||||
|
|
||||||
|
char* unsigned_event_json = cJSON_PrintUnformatted(unsigned_event);
|
||||||
|
cJSON_Delete(unsigned_event);
|
||||||
|
|
||||||
|
const char* sign_params[] = { unsigned_event_json };
|
||||||
|
nostr_nip46_request_t sign_req;
|
||||||
|
rc = nostr_nip46_create_request("req-sign", NOSTR_NIP46_METHOD_SIGN_EVENT, sign_params, 1, &sign_req);
|
||||||
|
expect_int("build sign_event request", NOSTR_SUCCESS, rc);
|
||||||
|
|
||||||
|
nostr_nip46_response_t sign_resp;
|
||||||
|
rc = nostr_nip46_signer_handle_request(&ss, &sign_req, &sign_resp);
|
||||||
|
expect_int("handle sign_event request", NOSTR_SUCCESS, rc);
|
||||||
|
expect_true("sign_event response contains id field", sign_resp.result && strstr(sign_resp.result, "\"id\"") != NULL);
|
||||||
|
expect_true("sign_event response contains sig field", sign_resp.result && strstr(sign_resp.result, "\"sig\"") != NULL);
|
||||||
|
|
||||||
|
nostr_nip46_free_request(&sign_req);
|
||||||
|
nostr_nip46_free_response(&sign_resp);
|
||||||
|
free(unsigned_event_json);
|
||||||
|
|
||||||
|
nostr_nip46_signer_session_destroy(&ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
printf("🧪 NIP-46 Test Suite\n");
|
||||||
|
printf("===================\n");
|
||||||
|
|
||||||
|
int init_rc = nostr_init();
|
||||||
|
if (init_rc != NOSTR_SUCCESS) {
|
||||||
|
printf("❌ Failed to initialize nostr library: %s\n", nostr_strerror(init_rc));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
test_url_parsing_and_generation();
|
||||||
|
test_request_response_roundtrip();
|
||||||
|
test_event_encryption_flow();
|
||||||
|
test_signer_handle_request();
|
||||||
|
|
||||||
|
nostr_cleanup();
|
||||||
|
|
||||||
|
printf("\n=== RESULT ===\n");
|
||||||
|
printf("Passed %d / %d tests\n", tests_passed, tests_run);
|
||||||
|
|
||||||
|
return (tests_passed == tests_run) ? 0 : 1;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user