First commit on a late git install

This commit is contained in:
2025-08-09 10:23:28 -04:00
commit ca6b4754f9
88 changed files with 18219 additions and 0 deletions

824
nostr_core/core.c Normal file
View File

@@ -0,0 +1,824 @@
/*
* NOSTR Core Library Implementation - Core Functionality
*
* Self-contained crypto implementation (no external crypto dependencies)
*
* This file contains:
* - NIP-19: Bech32-encoded Entities
* - NIP-01: Basic Protocol Flow
* - NIP-06: Key Derivation from Mnemonic
* - NIP-10: Text Notes (Kind 1)
* - NIP-13: Proof of Work
* - General Utilities
* - Identity Management
* - Single Relay Communication
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>
// Our self-contained crypto implementation
#include "nostr_crypto.h"
// Our production-ready WebSocket implementation
#include "../nostr_websocket/nostr_websocket_tls.h"
// cJSON for JSON handling
#include "../cjson/cJSON.h"
// Forward declarations for bech32 functions (used by NIP-06 functions)
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad);
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len);
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// GENERAL UTILITIES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex) {
for (size_t i = 0; i < len; i++) {
sprintf(hex + i * 2, "%02x", bytes[i]);
}
hex[len * 2] = '\0';
}
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len) {
if (strlen(hex) != len * 2) {
return NOSTR_ERROR_INVALID_INPUT;
}
for (size_t i = 0; i < len; i++) {
if (sscanf(hex + i * 2, "%02hhx", &bytes[i]) != 1) {
return NOSTR_ERROR_INVALID_INPUT;
}
}
return NOSTR_SUCCESS;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-01: BASIC PROTOCOL FLOW
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
int nostr_init(void) {
if (nostr_crypto_init() != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
void nostr_cleanup(void) {
nostr_crypto_cleanup();
}
const char* nostr_strerror(int error_code) {
switch (error_code) {
case NOSTR_SUCCESS: return "Success";
case NOSTR_ERROR_INVALID_INPUT: return "Invalid input";
case NOSTR_ERROR_CRYPTO_FAILED: return "Cryptographic operation failed";
case NOSTR_ERROR_MEMORY_FAILED: return "Memory allocation failed";
case NOSTR_ERROR_IO_FAILED: return "I/O operation failed";
case NOSTR_ERROR_NETWORK_FAILED: return "Network operation failed";
case NOSTR_ERROR_NIP04_INVALID_FORMAT: return "NIP-04 invalid format";
case NOSTR_ERROR_NIP04_DECRYPT_FAILED: return "NIP-04 decryption failed";
case NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL: return "NIP-04 buffer too small";
default: return "Unknown error";
}
}
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
if (!private_key) {
return NULL;
}
if (!content) {
content = ""; // Default to empty content
}
// Convert private key to public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NULL;
}
// Convert public key to hex
char pubkey_hex[65];
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
// Create event structure
cJSON* event = cJSON_CreateObject();
if (!event) {
return NULL;
}
// Use provided timestamp or current time if timestamp is 0
time_t event_time = (timestamp == 0) ? time(NULL) : timestamp;
cJSON_AddStringToObject(event, "pubkey", pubkey_hex);
cJSON_AddNumberToObject(event, "created_at", (double)event_time);
cJSON_AddNumberToObject(event, "kind", kind);
// Add tags (copy provided tags or create empty array)
if (tags) {
cJSON_AddItemToObject(event, "tags", cJSON_Duplicate(tags, 1));
} else {
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
}
cJSON_AddStringToObject(event, "content", content);
// ============================================================================
// INLINE SERIALIZATION AND SIGNING LOGIC
// ============================================================================
// Get event fields for serialization
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) {
cJSON_Delete(event);
return NULL;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
cJSON_Delete(event);
return NULL;
}
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
cJSON_Delete(serialize_array);
if (!serialize_string) {
cJSON_Delete(event);
return NULL;
}
// Hash the serialized event
unsigned char event_hash[32];
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
free(serialize_string);
cJSON_Delete(event);
return NULL;
}
// Convert hash to hex for event ID
char event_id[65];
nostr_bytes_to_hex(event_hash, 32, event_id);
// Sign the hash using ECDSA
unsigned char signature[64];
if (nostr_ec_sign(private_key, event_hash, signature) != 0) {
free(serialize_string);
cJSON_Delete(event);
return NULL;
}
// Convert signature to hex
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
// Add ID and signature to the event
cJSON_AddStringToObject(event, "id", event_id);
cJSON_AddStringToObject(event, "sig", sig_hex);
free(serialize_string);
return event;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-06: KEY DERIVATION FROM MNEMONIC
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key) {
if (!private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate random entropy using /dev/urandom
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
return NOSTR_ERROR_IO_FAILED;
}
if (fread(private_key, 1, 32, urandom) != 32) {
fclose(urandom);
return NOSTR_ERROR_IO_FAILED;
}
fclose(urandom);
// Validate private key
if (nostr_ec_private_key_verify(private_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Generate public key from private key (already x-only for NOSTR)
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key) {
if (!mnemonic || mnemonic_size < 256 || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate entropy for 12-word mnemonic
unsigned char entropy[16];
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
return NOSTR_ERROR_IO_FAILED;
}
if (fread(entropy, 1, sizeof(entropy), urandom) != sizeof(entropy)) {
fclose(urandom);
return NOSTR_ERROR_IO_FAILED;
}
fclose(urandom);
// Generate mnemonic from entropy
if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Derive keys from the generated mnemonic
return nostr_derive_keys_from_mnemonic(mnemonic, account, private_key, public_key);
}
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key) {
if (!mnemonic || !private_key || !public_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate mnemonic
if (nostr_bip39_mnemonic_validate(mnemonic) != 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Convert mnemonic to seed
unsigned char seed[64];
if (nostr_bip39_mnemonic_to_seed(mnemonic, "", seed, sizeof(seed)) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Derive master key from seed
nostr_hd_key_t master_key;
if (nostr_bip32_key_from_seed(seed, sizeof(seed), &master_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// NIP-06 path: m/44'/1237'/account'/0/0
nostr_hd_key_t derived_key;
uint32_t path[] = {
0x80000000 + 44, // 44' (hardened)
0x80000000 + 1237, // 1237' (hardened)
0x80000000 + account, // account' (hardened)
0, // 0 (not hardened)
0 // 0 (not hardened)
};
if (nostr_bip32_derive_path(&master_key, path, sizeof(path) / sizeof(path[0]), &derived_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Extract private key and public key
memcpy(private_key, derived_key.private_key, 32);
memcpy(public_key, derived_key.public_key + 1, 32); // Remove compression prefix for x-only
return NOSTR_SUCCESS;
}
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output) {
if (!key || !hrp || !output) {
return NOSTR_ERROR_INVALID_INPUT;
}
uint8_t conv[64];
size_t conv_len;
if (!convert_bits(conv, &conv_len, 5, key, 32, 8, 1)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
if (!bech32_encode(output, hrp, conv, conv_len)) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
nostr_input_type_t nostr_detect_input_type(const char* input) {
if (!input || strlen(input) == 0) {
return NOSTR_INPUT_UNKNOWN;
}
size_t len = strlen(input);
// Check for bech32 nsec
if (len > 5 && strncmp(input, "nsec1", 5) == 0) {
return NOSTR_INPUT_NSEC_BECH32;
}
// Check for hex nsec (64 characters, all hex)
if (len == 64) {
int is_hex = 1;
for (size_t i = 0; i < len; i++) {
if (!isxdigit(input[i])) {
is_hex = 0;
break;
}
}
if (is_hex) {
return NOSTR_INPUT_NSEC_HEX;
}
}
// Check for mnemonic (space-separated words)
int word_count = 0;
char temp[1024];
strncpy(temp, input, sizeof(temp) - 1);
temp[sizeof(temp) - 1] = '\0';
char* token = strtok(temp, " ");
while (token != NULL) {
word_count++;
token = strtok(NULL, " ");
}
// BIP39 mnemonics are typically 12, 18, or 24 words
if (word_count >= 12 && word_count <= 24) {
return NOSTR_INPUT_MNEMONIC;
}
return NOSTR_INPUT_UNKNOWN;
}
int nostr_decode_nsec(const char* input, unsigned char* private_key) {
if (!input || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
nostr_input_type_t type = nostr_detect_input_type(input);
if (type == NOSTR_INPUT_NSEC_HEX) {
if (nostr_hex_to_bytes(input, private_key, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else if (type == NOSTR_INPUT_NSEC_BECH32) {
size_t decoded_len;
if (!bech32_decode(input, "nsec", private_key, &decoded_len)) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (decoded_len != 32) {
return NOSTR_ERROR_INVALID_INPUT;
}
} else {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate the private key
if (nostr_ec_private_key_verify(private_key) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return NOSTR_SUCCESS;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-13: PROOF OF WORK
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Count leading zero bits in a hash (NIP-13 reference implementation)
*/
static int zero_bits(unsigned char b) {
int n = 0;
if (b == 0)
return 8;
while (b >>= 1)
n++;
return 7-n;
}
/**
* Find the number of leading zero bits in a hash (NIP-13 reference implementation)
*/
static int count_leading_zero_bits(unsigned char *hash) {
int bits, total, i;
for (i = 0, total = 0; i < 32; i++) {
bits = zero_bits(hash[i]);
total += bits;
if (bits != 8)
break;
}
return total;
}
/**
* Add or update nonce tag with target difficulty
*/
static int update_nonce_tag_with_difficulty(cJSON* tags, uint64_t nonce, int target_difficulty) {
if (!tags) return -1;
// Look for existing nonce tag and remove it
cJSON* tag = NULL;
int index = 0;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (tag_type && cJSON_IsString(tag_type) &&
strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) {
// Remove existing nonce tag
cJSON_DetachItemFromArray(tags, index);
cJSON_Delete(tag);
break;
}
}
index++;
}
// Add new nonce tag with format: ["nonce", "<nonce>", "<target_difficulty>"]
cJSON* nonce_tag = cJSON_CreateArray();
if (!nonce_tag) return -1;
char nonce_str[32];
char difficulty_str[16];
snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce);
snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty);
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str));
cJSON_AddItemToArray(tags, nonce_tag);
return 0;
}
/**
* Helper function to replace event content with successful PoW result
*/
static void replace_event_content(cJSON* target_event, cJSON* source_event) {
// Remove old fields
cJSON_DeleteItemFromObject(target_event, "id");
cJSON_DeleteItemFromObject(target_event, "sig");
cJSON_DeleteItemFromObject(target_event, "tags");
cJSON_DeleteItemFromObject(target_event, "created_at");
// Copy new fields from successful event
cJSON* id = cJSON_GetObjectItem(source_event, "id");
cJSON* sig = cJSON_GetObjectItem(source_event, "sig");
cJSON* tags = cJSON_GetObjectItem(source_event, "tags");
cJSON* created_at = cJSON_GetObjectItem(source_event, "created_at");
if (id) cJSON_AddItemToObject(target_event, "id", cJSON_Duplicate(id, 1));
if (sig) cJSON_AddItemToObject(target_event, "sig", cJSON_Duplicate(sig, 1));
if (tags) cJSON_AddItemToObject(target_event, "tags", cJSON_Duplicate(tags, 1));
if (created_at) cJSON_AddItemToObject(target_event, "created_at", cJSON_Duplicate(created_at, 1));
}
/**
* Add NIP-13 Proof of Work to an event
*
* @param event The event to add proof of work to
* @param private_key The private key for re-signing the event
* @param target_difficulty Target number of leading zero bits (default: 4 if 0)
* @param progress_callback Optional callback for mining progress
* @param user_data User data for progress callback
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty,
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
void* user_data) {
if (!event || !private_key) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Set default difficulty if not specified (but allow 0 to disable PoW)
if (target_difficulty < 0) {
target_difficulty = 4;
}
// If target_difficulty is 0, skip proof of work entirely
if (target_difficulty == 0) {
return NOSTR_SUCCESS;
}
// Extract event data for reconstruction
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
if (!kind_item || !content_item || !created_at_item || !tags_item) {
return NOSTR_ERROR_INVALID_INPUT;
}
int kind = (int)cJSON_GetNumberValue(kind_item);
const char* content = cJSON_GetStringValue(content_item);
time_t original_timestamp = (time_t)cJSON_GetNumberValue(created_at_item);
uint64_t nonce = 0;
int attempts = 0;
int max_attempts = 10000000;
time_t current_timestamp = original_timestamp;
// PoW difficulty tracking variables
int best_difficulty_this_round = 0;
int best_difficulty_overall = 0;
// Mining loop
while (attempts < max_attempts) {
// Update timestamp every 10,000 iterations
if (attempts % 10000 == 0) {
current_timestamp = time(NULL);
#ifdef ENABLE_DEBUG_LOGGING
FILE* f = fopen("debug.log", "a");
if (f) {
fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n",
attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty);
fclose(f);
}
#endif
// Reset best difficulty for the new round
best_difficulty_this_round = 0;
}
// Create working copy of tags and add nonce
cJSON* working_tags = cJSON_Duplicate(tags_item, 1);
if (!working_tags) {
return NOSTR_ERROR_MEMORY_FAILED;
}
if (update_nonce_tag_with_difficulty(working_tags, nonce, target_difficulty) != 0) {
cJSON_Delete(working_tags);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Create and sign new event with current nonce
cJSON* test_event = nostr_create_and_sign_event(kind, content, working_tags,
private_key, current_timestamp);
cJSON_Delete(working_tags);
if (!test_event) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Check PoW difficulty
cJSON* id_item = cJSON_GetObjectItem(test_event, "id");
if (!id_item || !cJSON_IsString(id_item)) {
cJSON_Delete(test_event);
return NOSTR_ERROR_CRYPTO_FAILED;
}
const char* event_id = cJSON_GetStringValue(id_item);
unsigned char hash[32];
if (nostr_hex_to_bytes(event_id, hash, 32) != NOSTR_SUCCESS) {
cJSON_Delete(test_event);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Count leading zero bits using NIP-13 method
int current_difficulty = count_leading_zero_bits(hash);
// Update difficulty tracking
if (current_difficulty > best_difficulty_this_round) {
best_difficulty_this_round = current_difficulty;
}
if (current_difficulty > best_difficulty_overall) {
best_difficulty_overall = current_difficulty;
}
// Call progress callback if provided
if (progress_callback) {
progress_callback(current_difficulty, nonce, user_data);
}
// Check if we've reached the target
if (current_difficulty >= target_difficulty) {
#ifdef ENABLE_DEBUG_LOGGING
FILE* f = fopen("debug.log", "a");
if (f) {
fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n",
current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1);
// Print the final event JSON
char* event_json = cJSON_Print(test_event);
if (event_json) {
fprintf(f, "Final event: %s\n", event_json);
free(event_json);
}
fclose(f);
}
#endif
// Copy successful result back to input event
replace_event_content(event, test_event);
cJSON_Delete(test_event);
return NOSTR_SUCCESS;
}
cJSON_Delete(test_event);
nonce++;
attempts++;
}
#ifdef ENABLE_DEBUG_LOGGING
// Debug logging - failure
FILE* f = fopen("debug.log", "a");
if (f) {
fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts);
fclose(f);
}
#endif
// If we reach here, we've exceeded max attempts
return NOSTR_ERROR_CRYPTO_FAILED;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-19: BECH32-ENCODED ENTITIES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
#define BECH32_CONST 1
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
static const int8_t bech32_charset_rev[128] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
};
static uint32_t bech32_polymod_step(uint32_t pre) {
uint8_t b = pre >> 25;
return ((pre & 0x1FFFFFF) << 5) ^
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
(-((b >> 1) & 1) & 0x26508e6dUL) ^
(-((b >> 2) & 1) & 0x1ea119faUL) ^
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
(-((b >> 4) & 1) & 0x2a1462b3UL);
}
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
*outlen = 0;
while (inlen--) {
val = (val << inbits) | *(in++);
bits += inbits;
while (bits >= outbits) {
bits -= outbits;
out[(*outlen)++] = (val >> bits) & maxv;
}
}
if (pad) {
if (bits) {
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
}
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
return 0;
}
return 1;
}
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
uint32_t chk = 1;
size_t i, hrp_len = strlen(hrp);
for (i = 0; i < hrp_len; ++i) {
int ch = hrp[i];
if (ch < 33 || ch > 126) return 0;
if (ch >= 'A' && ch <= 'Z') return 0;
chk = bech32_polymod_step(chk) ^ (ch >> 5);
}
chk = bech32_polymod_step(chk);
for (i = 0; i < hrp_len; ++i) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
*(output++) = hrp[i];
}
*(output++) = '1';
for (i = 0; i < data_len; ++i) {
if (*data >> 5) return 0;
chk = bech32_polymod_step(chk) ^ (*data);
*(output++) = bech32_charset[*(data++)];
}
for (i = 0; i < 6; ++i) {
chk = bech32_polymod_step(chk);
}
chk ^= BECH32_CONST;
for (i = 0; i < 6; ++i) {
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
}
*output = 0;
return 1;
}
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
if (!input || !hrp || !data || !data_len) {
return 0;
}
size_t input_len = strlen(input);
size_t hrp_len = strlen(hrp);
if (input_len < hrp_len + 7) return 0;
if (strncmp(input, hrp, hrp_len) != 0) return 0;
if (input[hrp_len] != '1') return 0;
const char* data_part = input + hrp_len + 1;
size_t data_part_len = input_len - hrp_len - 1;
uint8_t values[256];
for (size_t i = 0; i < data_part_len; i++) {
unsigned char c = (unsigned char)data_part[i];
if (c >= 128) return 0;
int8_t val = bech32_charset_rev[c];
if (val == -1) return 0;
values[i] = (uint8_t)val;
}
if (data_part_len < 6) return 0;
uint32_t chk = 1;
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
}
chk = bech32_polymod_step(chk);
for (size_t i = 0; i < hrp_len; i++) {
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
}
for (size_t i = 0; i < data_part_len; i++) {
chk = bech32_polymod_step(chk) ^ values[i];
}
if (chk != BECH32_CONST) return 0;
size_t payload_len = data_part_len - 6;
size_t decoded_len;
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
return 0;
}
*data_len = decoded_len;
return 1;
}

BIN
nostr_core/core.o Normal file

Binary file not shown.

1284
nostr_core/core_relay_pool.c Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

610
nostr_core/core_relays.c Normal file
View File

@@ -0,0 +1,610 @@
/*
* NOSTR Core Library Implementation - Relay Pool Management
*
* This file contains:
* - Relay Pool Management
* - Pool connection management
* - Subscription handling
* - Event processing and dispatching
* - Statistics and latency tracking
* - Multi-relay query and publish functions
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
// Our production-ready WebSocket implementation
#include "../nostr_websocket/nostr_websocket_tls.h"
// cJSON for JSON handling
#include "../cjson/cJSON.h"
// =============================================================================
// TYPE DEFINITIONS FOR SYNCHRONOUS RELAY QUERIES
// =============================================================================
// Relay state enum (internal to this file)
typedef enum {
RELAY_STATE_CONNECTING,
RELAY_STATE_SUBSCRIBED, // REQ sent successfully
RELAY_STATE_ACTIVE, // Receiving events
RELAY_STATE_EOSE_RECEIVED, // End of stored events
RELAY_STATE_TIMED_OUT, // No response within timeout
RELAY_STATE_ERROR // Connection or protocol error
} relay_state_enum_t;
// Internal relay connection structure
typedef struct {
nostr_ws_client_t* client;
char* url;
time_t last_activity; // Last message received
time_t request_sent_time; // When REQ was sent
relay_state_enum_t state;
int events_received;
cJSON** events; // Array of events from this relay
int events_capacity; // Allocated capacity
char subscription_id[32]; // Unique subscription ID
} relay_connection_t;
// =============================================================================
// SYNCHRONOUS MULTI-RELAY QUERY WITH PROGRESS CALLBACKS
// =============================================================================
cJSON** synchronous_query_relays_with_progress(
const char** relay_urls,
int relay_count,
cJSON* filter,
relay_query_mode_t mode,
int* result_count,
int relay_timeout_seconds,
relay_progress_callback_t callback,
void* user_data) {
if (!relay_urls || relay_count <= 0 || !filter || !result_count) {
if (result_count) *result_count = 0;
return NULL;
}
*result_count = 0;
// Set default timeout if not specified
if (relay_timeout_seconds <= 0) {
relay_timeout_seconds = 2; // Default 2 seconds
}
// Initialize relay connections
relay_connection_t* relays = calloc(relay_count, sizeof(relay_connection_t));
if (!relays) {
return NULL;
}
// Setup connections
int active_relays = 0;
time_t start_time = time(NULL);
for (int i = 0; i < relay_count; i++) {
relays[i].url = strdup(relay_urls[i]);
relays[i].state = RELAY_STATE_CONNECTING;
relays[i].last_activity = start_time;
relays[i].events_capacity = 10;
relays[i].events = malloc(relays[i].events_capacity * sizeof(cJSON*));
// Generate unique subscription ID
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
"sync_%d_%ld", i, start_time);
if (callback) {
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
}
// Attempt connection
relays[i].client = nostr_ws_connect(relays[i].url);
if (relays[i].client) {
relays[i].state = RELAY_STATE_SUBSCRIBED;
relays[i].request_sent_time = time(NULL);
relays[i].last_activity = relays[i].request_sent_time;
// Send REQ message
if (nostr_relay_send_req(relays[i].client, relays[i].subscription_id, filter) >= 0) {
active_relays++;
if (callback) {
callback(relays[i].url, "subscribed", NULL, 0, relay_count, 0, user_data);
}
} else {
relays[i].state = RELAY_STATE_ERROR;
if (callback) {
callback(relays[i].url, "error", NULL, 0, relay_count, 0, user_data);
}
}
} else {
relays[i].state = RELAY_STATE_ERROR;
if (callback) {
callback(relays[i].url, "error", NULL, 0, relay_count, 0, user_data);
}
}
}
if (active_relays == 0) {
// Cleanup and return
for (int i = 0; i < relay_count; i++) {
free(relays[i].url);
free(relays[i].events);
}
free(relays);
return NULL;
}
// Main polling loop
cJSON** result_array = NULL;
int total_unique_events = 0;
char seen_event_ids[1000][65]; // Simple deduplication
int seen_count = 0;
while (active_relays > 0) {
time_t current_time = time(NULL);
int completed_relays = relay_count - active_relays;
for (int i = 0; i < relay_count; i++) {
relay_connection_t* relay = &relays[i];
// Skip finished relays
if (relay->state == RELAY_STATE_EOSE_RECEIVED ||
relay->state == RELAY_STATE_TIMED_OUT ||
relay->state == RELAY_STATE_ERROR) {
continue;
}
// Check for timeout
time_t time_since_activity = current_time - relay->last_activity;
if (time_since_activity > relay_timeout_seconds) {
relay->state = RELAY_STATE_TIMED_OUT;
active_relays--;
if (callback) {
callback(relay->url, "timeout", NULL, relay->events_received,
relay_count, completed_relays + 1, user_data);
}
if (relay->client) {
nostr_relay_send_close(relay->client, relay->subscription_id);
nostr_ws_close(relay->client);
relay->client = NULL;
}
continue;
}
// Try to receive message (short timeout to keep UI responsive)
char buffer[8192];
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 50);
if (len > 0) {
buffer[len] = '\0';
relay->last_activity = current_time;
// Parse message
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
// Handle EVENT message
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
cJSON* event = cJSON_GetArrayItem(parsed, 2);
if (cJSON_IsString(sub_id_json) && event &&
strcmp(cJSON_GetStringValue(sub_id_json), relay->subscription_id) == 0) {
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
if (event_id_json && cJSON_IsString(event_id_json)) {
const char* event_id = cJSON_GetStringValue(event_id_json);
// Check for duplicate
int is_duplicate = 0;
for (int j = 0; j < seen_count; j++) {
if (strcmp(seen_event_ids[j], event_id) == 0) {
is_duplicate = 1;
break;
}
}
if (!is_duplicate && seen_count < 1000) {
// New event - add to seen list
strncpy(seen_event_ids[seen_count], event_id, 64);
seen_event_ids[seen_count][64] = '\0';
seen_count++;
total_unique_events++;
// Store event in relay's array
if (relay->events_received >= relay->events_capacity) {
relay->events_capacity *= 2;
relay->events = realloc(relay->events,
relay->events_capacity * sizeof(cJSON*));
}
relay->events[relay->events_received] = cJSON_Duplicate(event, 1);
relay->events_received++;
relay->state = RELAY_STATE_ACTIVE;
if (callback) {
callback(relay->url, "event_found", event_id,
relay->events_received, relay_count,
completed_relays, user_data);
}
// For FIRST_RESULT mode, return immediately
if (mode == RELAY_QUERY_FIRST_RESULT) {
result_array = malloc(sizeof(cJSON*));
if (result_array) {
result_array[0] = cJSON_Duplicate(event, 1);
*result_count = 1;
if (callback) {
callback(NULL, "first_result", event_id,
1, relay_count, completed_relays, user_data);
}
}
goto cleanup; // Break out of all loops
}
}
}
}
}
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
// Handle End of Stored Events
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(sub_id_json) &&
strcmp(cJSON_GetStringValue(sub_id_json), relay->subscription_id) == 0) {
relay->state = RELAY_STATE_EOSE_RECEIVED;
active_relays--;
if (callback) {
callback(relay->url, "eose", NULL, relay->events_received,
relay_count, completed_relays + 1, user_data);
}
// Send CLOSE and cleanup
nostr_relay_send_close(relay->client, relay->subscription_id);
nostr_ws_close(relay->client);
relay->client = NULL;
}
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
}
}
// Small sleep to prevent busy waiting
usleep(10000); // 10ms
}
// Handle different return modes
if (mode == RELAY_QUERY_MOST_RECENT && total_unique_events > 0) {
// Find the event with highest created_at
time_t most_recent_time = 0;
cJSON* most_recent_event = NULL;
const char* most_recent_id = NULL;
for (int i = 0; i < relay_count; i++) {
for (int j = 0; j < relays[i].events_received; j++) {
cJSON* event = relays[i].events[j];
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
if (created_at && cJSON_IsNumber(created_at)) {
time_t event_time = (time_t)cJSON_GetNumberValue(created_at);
if (event_time > most_recent_time) {
most_recent_time = event_time;
most_recent_event = event;
cJSON* id_json = cJSON_GetObjectItem(event, "id");
if (id_json && cJSON_IsString(id_json)) {
most_recent_id = cJSON_GetStringValue(id_json);
}
}
}
}
}
if (most_recent_event) {
result_array = malloc(sizeof(cJSON*));
if (result_array) {
result_array[0] = cJSON_Duplicate(most_recent_event, 1);
*result_count = 1;
if (callback) {
callback(NULL, "all_complete", most_recent_id, 1,
relay_count, relay_count, user_data);
}
}
}
} else if (mode == RELAY_QUERY_ALL_RESULTS && total_unique_events > 0) {
// Return ALL unique events
result_array = malloc(total_unique_events * sizeof(cJSON*));
if (result_array) {
int event_index = 0;
// Collect all unique events from all relays
for (int i = 0; i < relay_count; i++) {
for (int j = 0; j < relays[i].events_received; j++) {
cJSON* event = relays[i].events[j];
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
if (event_id_json && cJSON_IsString(event_id_json)) {
const char* event_id = cJSON_GetStringValue(event_id_json);
// Check if we already added this event ID
int already_added = 0;
for (int k = 0; k < event_index; k++) {
cJSON* existing_id = cJSON_GetObjectItem(result_array[k], "id");
if (existing_id && cJSON_IsString(existing_id) &&
strcmp(cJSON_GetStringValue(existing_id), event_id) == 0) {
already_added = 1;
break;
}
}
if (!already_added) {
result_array[event_index] = cJSON_Duplicate(event, 1);
event_index++;
}
}
}
}
*result_count = event_index;
if (callback) {
callback(NULL, "all_complete", NULL, total_unique_events,
relay_count, relay_count, user_data);
}
}
}
cleanup:
// Cleanup all relay connections and data
for (int i = 0; i < relay_count; i++) {
if (relays[i].client) {
nostr_relay_send_close(relays[i].client, relays[i].subscription_id);
nostr_ws_close(relays[i].client);
}
// Free stored events
for (int j = 0; j < relays[i].events_received; j++) {
if (relays[i].events[j]) {
cJSON_Delete(relays[i].events[j]);
}
}
free(relays[i].url);
free(relays[i].events);
}
free(relays);
return result_array;
}
// =============================================================================
// SYNCHRONOUS MULTI-RELAY PUBLISH WITH PROGRESS CALLBACKS
// =============================================================================
publish_result_t* synchronous_publish_event_with_progress(
const char** relay_urls,
int relay_count,
cJSON* event,
int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback,
void* user_data) {
if (!relay_urls || relay_count <= 0 || !event || !success_count) {
if (success_count) *success_count = 0;
return NULL;
}
*success_count = 0;
// Set default timeout if not specified
if (relay_timeout_seconds <= 0) {
relay_timeout_seconds = 5; // Default 5 seconds for publishing
}
// Initialize relay connections
relay_connection_t* relays = calloc(relay_count, sizeof(relay_connection_t));
if (!relays) {
return NULL;
}
// Initialize results array
publish_result_t* results = calloc(relay_count, sizeof(publish_result_t));
if (!results) {
free(relays);
return NULL;
}
// Get event ID for tracking
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
const char* event_id = NULL;
if (event_id_json && cJSON_IsString(event_id_json)) {
event_id = cJSON_GetStringValue(event_id_json);
}
// Setup connections
int active_relays = 0;
time_t start_time = time(NULL);
for (int i = 0; i < relay_count; i++) {
relays[i].url = strdup(relay_urls[i]);
relays[i].state = RELAY_STATE_CONNECTING;
relays[i].last_activity = start_time;
results[i] = PUBLISH_ERROR; // Default to error
if (callback) {
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
}
// Attempt connection
relays[i].client = nostr_ws_connect(relays[i].url);
if (relays[i].client) {
relays[i].state = RELAY_STATE_SUBSCRIBED; // Reuse for "publishing" state
relays[i].request_sent_time = time(NULL);
relays[i].last_activity = relays[i].request_sent_time;
// Send EVENT message
if (nostr_relay_send_event(relays[i].client, event) >= 0) {
active_relays++;
if (callback) {
callback(relays[i].url, "publishing", NULL, 0, relay_count, 0, user_data);
}
} else {
relays[i].state = RELAY_STATE_ERROR;
results[i] = PUBLISH_ERROR;
if (callback) {
callback(relays[i].url, "error", "Failed to send EVENT message",
0, relay_count, 0, user_data);
}
}
} else {
relays[i].state = RELAY_STATE_ERROR;
results[i] = PUBLISH_ERROR;
if (callback) {
callback(relays[i].url, "error", "Failed to connect", 0, relay_count, 0, user_data);
}
}
}
if (active_relays == 0) {
// Cleanup and return
for (int i = 0; i < relay_count; i++) {
free(relays[i].url);
}
free(relays);
return results; // Return error results
}
// Main polling loop
int completed_relays = relay_count - active_relays;
while (active_relays > 0) {
time_t current_time = time(NULL);
for (int i = 0; i < relay_count; i++) {
relay_connection_t* relay = &relays[i];
// Skip finished relays
if (relay->state == RELAY_STATE_EOSE_RECEIVED || // Reuse for "completed"
relay->state == RELAY_STATE_TIMED_OUT ||
relay->state == RELAY_STATE_ERROR) {
continue;
}
// Check for timeout
time_t time_since_activity = current_time - relay->last_activity;
if (time_since_activity > relay_timeout_seconds) {
relay->state = RELAY_STATE_TIMED_OUT;
results[i] = PUBLISH_TIMEOUT;
active_relays--;
completed_relays++;
if (callback) {
callback(relay->url, "timeout", "No response within timeout",
*success_count, relay_count, completed_relays, user_data);
}
if (relay->client) {
nostr_ws_close(relay->client);
relay->client = NULL;
}
continue;
}
// Try to receive message (short timeout to keep UI responsive)
char buffer[8192];
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 50);
if (len > 0) {
buffer[len] = '\0';
relay->last_activity = current_time;
// Parse message
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "OK") == 0) {
// Handle OK message: ["OK", <event_id>, <true|false>, <message>]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* ok_event_id = cJSON_GetArrayItem(parsed, 1);
cJSON* accepted = cJSON_GetArrayItem(parsed, 2);
cJSON* message = cJSON_GetArrayItem(parsed, 3);
// Verify this OK is for our event
if (ok_event_id && cJSON_IsString(ok_event_id) && event_id &&
strcmp(cJSON_GetStringValue(ok_event_id), event_id) == 0) {
relay->state = RELAY_STATE_EOSE_RECEIVED; // Reuse for "completed"
active_relays--;
completed_relays++;
const char* ok_message = "";
if (message && cJSON_IsString(message)) {
ok_message = cJSON_GetStringValue(message);
}
if (accepted && cJSON_IsBool(accepted) && cJSON_IsTrue(accepted)) {
// Event accepted
results[i] = PUBLISH_SUCCESS;
(*success_count)++;
if (callback) {
callback(relay->url, "accepted", ok_message,
*success_count, relay_count, completed_relays, user_data);
}
} else {
// Event rejected
results[i] = PUBLISH_REJECTED;
if (callback) {
callback(relay->url, "rejected", ok_message,
*success_count, relay_count, completed_relays, user_data);
}
}
// Close connection
nostr_ws_close(relay->client);
relay->client = NULL;
}
}
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
}
}
// Small sleep to prevent busy waiting
usleep(10000); // 10ms
}
// Final callback with summary
if (callback) {
callback(NULL, "all_complete", NULL, *success_count, relay_count, relay_count, user_data);
}
// Cleanup relay connections
for (int i = 0; i < relay_count; i++) {
if (relays[i].client) {
nostr_ws_close(relays[i].client);
}
free(relays[i].url);
}
free(relays);
return results;
}

BIN
nostr_core/core_relays.o Normal file

Binary file not shown.

400
nostr_core/nostr_aes.c Normal file
View File

@@ -0,0 +1,400 @@
/*
* NOSTR AES Implementation
*
* Based on tiny-AES-c by kokke (public domain)
* Configured specifically for NIP-04: AES-256-CBC only
*
* This is an implementation of the AES algorithm, specifically CBC mode.
* Configured for AES-256 as required by NIP-04.
*/
#include <string.h> // CBC mode, for memset
#include "nostr_aes.h"
// The number of columns comprising a state in AES. This is a constant in AES. Value=4
#define Nb 4
#if defined(AES256) && (AES256 == 1)
#define Nk 8
#define Nr 14
#elif defined(AES192) && (AES192 == 1)
#define Nk 6
#define Nr 12
#else
#define Nk 4 // The number of 32 bit words in a key.
#define Nr 10 // The number of rounds in AES Cipher.
#endif
// state - array holding the intermediate results during decryption.
typedef uint8_t state_t[4][4];
// The lookup-tables are marked const so they can be placed in read-only storage instead of RAM
static const uint8_t sbox[256] = {
//0 1 2 3 4 5 6 7 8 9 A B C D E F
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 };
static const uint8_t rsbox[256] = {
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d };
// The round constant word array, Rcon[i], contains the values given by
// x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8)
static const uint8_t Rcon[11] = {
0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 };
#define getSBoxValue(num) (sbox[(num)])
#define getSBoxInvert(num) (rsbox[(num)])
// This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states.
static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key)
{
unsigned i, j, k;
uint8_t tempa[4]; // Used for the column/row operations
// The first round key is the key itself.
for (i = 0; i < Nk; ++i)
{
RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
}
// All other round keys are found from the previous round keys.
for (i = Nk; i < Nb * (Nr + 1); ++i)
{
{
k = (i - 1) * 4;
tempa[0]=RoundKey[k + 0];
tempa[1]=RoundKey[k + 1];
tempa[2]=RoundKey[k + 2];
tempa[3]=RoundKey[k + 3];
}
if (i % Nk == 0)
{
// This function shifts the 4 bytes in a word to the left once.
// [a0,a1,a2,a3] becomes [a1,a2,a3,a0]
// Function RotWord()
{
const uint8_t u8tmp = tempa[0];
tempa[0] = tempa[1];
tempa[1] = tempa[2];
tempa[2] = tempa[3];
tempa[3] = u8tmp;
}
// SubWord() is a function that takes a four-byte input word and
// applies the S-box to each of the four bytes to produce an output word.
// Function Subword()
{
tempa[0] = getSBoxValue(tempa[0]);
tempa[1] = getSBoxValue(tempa[1]);
tempa[2] = getSBoxValue(tempa[2]);
tempa[3] = getSBoxValue(tempa[3]);
}
tempa[0] = tempa[0] ^ Rcon[i/Nk];
}
#if defined(AES256) && (AES256 == 1)
if (i % Nk == 4)
{
// Function Subword()
{
tempa[0] = getSBoxValue(tempa[0]);
tempa[1] = getSBoxValue(tempa[1]);
tempa[2] = getSBoxValue(tempa[2]);
tempa[3] = getSBoxValue(tempa[3]);
}
}
#endif
j = i * 4; k=(i - Nk) * 4;
RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0];
RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1];
RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2];
RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3];
}
}
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key)
{
KeyExpansion(ctx->RoundKey, key);
}
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
{
KeyExpansion(ctx->RoundKey, key);
memcpy (ctx->Iv, iv, AES_BLOCKLEN);
}
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv)
{
memcpy (ctx->Iv, iv, AES_BLOCKLEN);
}
// This function adds the round key to state.
// The round key is added to the state by an XOR function.
static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey)
{
uint8_t i,j;
for (i = 0; i < 4; ++i)
{
for (j = 0; j < 4; ++j)
{
(*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j];
}
}
}
// The SubBytes Function Substitutes the values in the
// state matrix with values in an S-box.
static void SubBytes(state_t* state)
{
uint8_t i, j;
for (i = 0; i < 4; ++i)
{
for (j = 0; j < 4; ++j)
{
(*state)[j][i] = getSBoxValue((*state)[j][i]);
}
}
}
// The ShiftRows() function shifts the rows in the state to the left.
// Each row is shifted with different offset.
// Offset = Row number. So the first row is not shifted.
static void ShiftRows(state_t* state)
{
uint8_t temp;
// Rotate first row 1 columns to left
temp = (*state)[0][1];
(*state)[0][1] = (*state)[1][1];
(*state)[1][1] = (*state)[2][1];
(*state)[2][1] = (*state)[3][1];
(*state)[3][1] = temp;
// Rotate second row 2 columns to left
temp = (*state)[0][2];
(*state)[0][2] = (*state)[2][2];
(*state)[2][2] = temp;
temp = (*state)[1][2];
(*state)[1][2] = (*state)[3][2];
(*state)[3][2] = temp;
// Rotate third row 3 columns to left
temp = (*state)[0][3];
(*state)[0][3] = (*state)[3][3];
(*state)[3][3] = (*state)[2][3];
(*state)[2][3] = (*state)[1][3];
(*state)[1][3] = temp;
}
static uint8_t xtime(uint8_t x)
{
return ((x<<1) ^ (((x>>7) & 1) * 0x1b));
}
// MixColumns function mixes the columns of the state matrix
static void MixColumns(state_t* state)
{
uint8_t i;
uint8_t Tmp, Tm, t;
for (i = 0; i < 4; ++i)
{
t = (*state)[i][0];
Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ;
Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ;
Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ;
Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ;
Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ;
}
}
// Multiply is used to multiply numbers in the field GF(2^8)
#define Multiply(x, y) \
( ((y & 1) * x) ^ \
((y>>1 & 1) * xtime(x)) ^ \
((y>>2 & 1) * xtime(xtime(x))) ^ \
((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \
((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \
// MixColumns function mixes the columns of the state matrix.
static void InvMixColumns(state_t* state)
{
int i;
uint8_t a, b, c, d;
for (i = 0; i < 4; ++i)
{
a = (*state)[i][0];
b = (*state)[i][1];
c = (*state)[i][2];
d = (*state)[i][3];
(*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09);
(*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d);
(*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b);
(*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e);
}
}
// The SubBytes Function Substitutes the values in the
// state matrix with values in an S-box.
static void InvSubBytes(state_t* state)
{
uint8_t i, j;
for (i = 0; i < 4; ++i)
{
for (j = 0; j < 4; ++j)
{
(*state)[j][i] = getSBoxInvert((*state)[j][i]);
}
}
}
static void InvShiftRows(state_t* state)
{
uint8_t temp;
// Rotate first row 1 columns to right
temp = (*state)[3][1];
(*state)[3][1] = (*state)[2][1];
(*state)[2][1] = (*state)[1][1];
(*state)[1][1] = (*state)[0][1];
(*state)[0][1] = temp;
// Rotate second row 2 columns to right
temp = (*state)[0][2];
(*state)[0][2] = (*state)[2][2];
(*state)[2][2] = temp;
temp = (*state)[1][2];
(*state)[1][2] = (*state)[3][2];
(*state)[3][2] = temp;
// Rotate third row 3 columns to right
temp = (*state)[0][3];
(*state)[0][3] = (*state)[1][3];
(*state)[1][3] = (*state)[2][3];
(*state)[2][3] = (*state)[3][3];
(*state)[3][3] = temp;
}
// Cipher is the main function that encrypts the PlainText.
static void Cipher(state_t* state, const uint8_t* RoundKey)
{
uint8_t round = 0;
// Add the First round key to the state before starting the rounds.
AddRoundKey(0, state, RoundKey);
// There will be Nr rounds.
// The first Nr-1 rounds are identical.
// These Nr rounds are executed in the loop below.
// Last one without MixColumns()
for (round = 1; ; ++round)
{
SubBytes(state);
ShiftRows(state);
if (round == Nr) {
break;
}
MixColumns(state);
AddRoundKey(round, state, RoundKey);
}
// Add round key to last round
AddRoundKey(Nr, state, RoundKey);
}
static void InvCipher(state_t* state, const uint8_t* RoundKey)
{
uint8_t round = 0;
// Add the First round key to the state before starting the rounds.
AddRoundKey(Nr, state, RoundKey);
// There will be Nr rounds.
// The first Nr-1 rounds are identical.
// These Nr rounds are executed in the loop below.
// Last one without InvMixColumn()
for (round = (Nr - 1); ; --round)
{
InvShiftRows(state);
InvSubBytes(state);
AddRoundKey(round, state, RoundKey);
if (round == 0) {
break;
}
InvMixColumns(state);
}
}
static void XorWithIv(uint8_t* buf, const uint8_t* Iv)
{
uint8_t i;
for (i = 0; i < AES_BLOCKLEN; ++i) // The block in AES is always 128bit no matter the key size
{
buf[i] ^= Iv[i];
}
}
void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, size_t length)
{
size_t i;
uint8_t *Iv = ctx->Iv;
for (i = 0; i < length; i += AES_BLOCKLEN)
{
XorWithIv(buf, Iv);
Cipher((state_t*)buf, ctx->RoundKey);
Iv = buf;
buf += AES_BLOCKLEN;
}
/* store Iv in ctx for next call */
memcpy(ctx->Iv, Iv, AES_BLOCKLEN);
}
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
{
size_t i;
uint8_t storeNextIv[AES_BLOCKLEN];
for (i = 0; i < length; i += AES_BLOCKLEN)
{
memcpy(storeNextIv, buf, AES_BLOCKLEN);
InvCipher((state_t*)buf, ctx->RoundKey);
XorWithIv(buf, ctx->Iv);
memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN);
buf += AES_BLOCKLEN;
}
}

53
nostr_core/nostr_aes.h Normal file
View File

@@ -0,0 +1,53 @@
#ifndef _NOSTR_AES_H_
#define _NOSTR_AES_H_
#include <stdint.h>
#include <stddef.h>
// Configure for NIP-04 requirements: AES-256-CBC only
#define CBC 1
#define ECB 0
#define CTR 0
// Configure for AES-256 (required by NIP-04)
#define AES128 0
#define AES192 0
#define AES256 1
#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only
#if defined(AES256) && (AES256 == 1)
#define AES_KEYLEN 32
#define AES_keyExpSize 240
#elif defined(AES192) && (AES192 == 1)
#define AES_KEYLEN 24
#define AES_keyExpSize 208
#else
#define AES_KEYLEN 16 // Key length in bytes
#define AES_keyExpSize 176
#endif
struct AES_ctx
{
uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif
#if defined(CBC) && (CBC == 1)
// buffer size MUST be multiple of AES_BLOCKLEN;
// Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme
// NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv()
// no IV should ever be reused with the same key
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CBC) && (CBC == 1)
#endif // _NOSTR_AES_H_

BIN
nostr_core/nostr_aes.o Normal file

Binary file not shown.

163
nostr_core/nostr_chacha20.c Normal file
View File

@@ -0,0 +1,163 @@
/*
* nostr_chacha20.c - ChaCha20 stream cipher implementation
*
* Implementation based on RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols"
*
* This implementation is adapted from the RFC 8439 reference specification.
* It prioritizes correctness and clarity over performance optimization.
*/
#include "nostr_chacha20.h"
#include <string.h>
/*
* ============================================================================
* UTILITY MACROS AND FUNCTIONS
* ============================================================================
*/
/* Left rotate a 32-bit value by n bits */
#define ROTLEFT(a, b) (((a) << (b)) | ((a) >> (32 - (b))))
/* Convert 4 bytes to 32-bit little-endian */
static uint32_t bytes_to_u32_le(const uint8_t *bytes) {
return ((uint32_t)bytes[0]) |
((uint32_t)bytes[1] << 8) |
((uint32_t)bytes[2] << 16) |
((uint32_t)bytes[3] << 24);
}
/* Convert 32-bit to 4 bytes little-endian */
static void u32_to_bytes_le(uint32_t val, uint8_t *bytes) {
bytes[0] = (uint8_t)(val & 0xff);
bytes[1] = (uint8_t)((val >> 8) & 0xff);
bytes[2] = (uint8_t)((val >> 16) & 0xff);
bytes[3] = (uint8_t)((val >> 24) & 0xff);
}
/*
* ============================================================================
* CHACHA20 CORE FUNCTIONS
* ============================================================================
*/
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d) {
state[a] += state[b];
state[d] ^= state[a];
state[d] = ROTLEFT(state[d], 16);
state[c] += state[d];
state[b] ^= state[c];
state[b] = ROTLEFT(state[b], 12);
state[a] += state[b];
state[d] ^= state[a];
state[d] = ROTLEFT(state[d], 8);
state[c] += state[d];
state[b] ^= state[c];
state[b] = ROTLEFT(state[b], 7);
}
void chacha20_init_state(uint32_t state[16], const uint8_t key[32],
uint32_t counter, const uint8_t nonce[12]) {
/* ChaCha20 constants "expand 32-byte k" */
state[0] = 0x61707865;
state[1] = 0x3320646e;
state[2] = 0x79622d32;
state[3] = 0x6b206574;
/* Key (8 words) */
state[4] = bytes_to_u32_le(key + 0);
state[5] = bytes_to_u32_le(key + 4);
state[6] = bytes_to_u32_le(key + 8);
state[7] = bytes_to_u32_le(key + 12);
state[8] = bytes_to_u32_le(key + 16);
state[9] = bytes_to_u32_le(key + 20);
state[10] = bytes_to_u32_le(key + 24);
state[11] = bytes_to_u32_le(key + 28);
/* Counter (1 word) */
state[12] = counter;
/* Nonce (3 words) */
state[13] = bytes_to_u32_le(nonce + 0);
state[14] = bytes_to_u32_le(nonce + 4);
state[15] = bytes_to_u32_le(nonce + 8);
}
void chacha20_serialize_state(const uint32_t state[16], uint8_t output[64]) {
for (int i = 0; i < 16; i++) {
u32_to_bytes_le(state[i], output + (i * 4));
}
}
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]) {
uint32_t state[16];
uint32_t initial_state[16];
/* Initialize state */
chacha20_init_state(state, key, counter, nonce);
/* Save initial state for later addition */
memcpy(initial_state, state, sizeof(initial_state));
/* Perform 20 rounds (10 iterations of the 8 quarter rounds) */
for (int i = 0; i < 10; i++) {
/* Column rounds */
chacha20_quarter_round(state, 0, 4, 8, 12);
chacha20_quarter_round(state, 1, 5, 9, 13);
chacha20_quarter_round(state, 2, 6, 10, 14);
chacha20_quarter_round(state, 3, 7, 11, 15);
/* Diagonal rounds */
chacha20_quarter_round(state, 0, 5, 10, 15);
chacha20_quarter_round(state, 1, 6, 11, 12);
chacha20_quarter_round(state, 2, 7, 8, 13);
chacha20_quarter_round(state, 3, 4, 9, 14);
}
/* Add initial state back (prevents slide attacks) */
for (int i = 0; i < 16; i++) {
state[i] += initial_state[i];
}
/* Serialize to output bytes */
chacha20_serialize_state(state, output);
return 0;
}
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], const uint8_t* input,
uint8_t* output, size_t length) {
uint8_t keystream[CHACHA20_BLOCK_SIZE];
size_t offset = 0;
while (length > 0) {
/* Generate keystream block */
int ret = chacha20_block(key, counter, nonce, keystream);
if (ret != 0) {
return ret;
}
/* XOR with input to produce output */
size_t block_len = (length < CHACHA20_BLOCK_SIZE) ? length : CHACHA20_BLOCK_SIZE;
for (size_t i = 0; i < block_len; i++) {
output[offset + i] = input[offset + i] ^ keystream[i];
}
/* Move to next block */
offset += block_len;
length -= block_len;
counter++;
/* Check for counter overflow */
if (counter == 0) {
return -1; /* Counter wrapped around */
}
}
return 0;
}

115
nostr_core/nostr_chacha20.h Normal file
View File

@@ -0,0 +1,115 @@
/*
* nostr_chacha20.h - ChaCha20 stream cipher implementation
*
* Implementation based on RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols"
*
* This is a small, portable implementation for NIP-44 support in the NOSTR library.
* The implementation prioritizes correctness and simplicity over performance.
*/
#ifndef NOSTR_CHACHA20_H
#define NOSTR_CHACHA20_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* ============================================================================
* CONSTANTS AND DEFINITIONS
* ============================================================================
*/
#define CHACHA20_KEY_SIZE 32 /* 256 bits */
#define CHACHA20_NONCE_SIZE 12 /* 96 bits */
#define CHACHA20_BLOCK_SIZE 64 /* 512 bits */
/*
* ============================================================================
* CORE CHACHA20 FUNCTIONS
* ============================================================================
*/
/**
* ChaCha20 quarter round operation
*
* Operates on four 32-bit words performing the core ChaCha20 quarter round:
* a += b; d ^= a; d <<<= 16;
* c += d; b ^= c; b <<<= 12;
* a += b; d ^= a; d <<<= 8;
* c += d; b ^= c; b <<<= 7;
*
* @param state[in,out] ChaCha state as 16 32-bit words
* @param a, b, c, d Indices into state array for quarter round
*/
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d);
/**
* ChaCha20 block function
*
* Transforms a 64-byte input block using ChaCha20 algorithm with 20 rounds.
*
* @param key[in] 32-byte key
* @param counter[in] 32-bit block counter
* @param nonce[in] 12-byte nonce
* @param output[out] 64-byte output buffer
* @return 0 on success, negative on error
*/
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]);
/**
* ChaCha20 encryption/decryption
*
* Encrypts or decrypts data using ChaCha20 stream cipher.
* Since ChaCha20 is a stream cipher, encryption and decryption are the same operation.
*
* @param key[in] 32-byte key
* @param counter[in] Initial 32-bit counter value
* @param nonce[in] 12-byte nonce
* @param input[in] Input data to encrypt/decrypt
* @param output[out] Output buffer (can be same as input)
* @param length[in] Length of input data in bytes
* @return 0 on success, negative on error
*/
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], const uint8_t* input,
uint8_t* output, size_t length);
/*
* ============================================================================
* UTILITY FUNCTIONS
* ============================================================================
*/
/**
* Initialize ChaCha20 state matrix
*
* Sets up the initial 16-word state matrix with constants, key, counter, and nonce.
*
* @param state[out] 16-word state array to initialize
* @param key[in] 32-byte key
* @param counter[in] 32-bit block counter
* @param nonce[in] 12-byte nonce
*/
void chacha20_init_state(uint32_t state[16], const uint8_t key[32],
uint32_t counter, const uint8_t nonce[12]);
/**
* Serialize ChaCha20 state to bytes
*
* Converts 16 32-bit words to 64 bytes in little-endian format.
*
* @param state[in] 16-word state array
* @param output[out] 64-byte output buffer
*/
void chacha20_serialize_state(const uint32_t state[16], uint8_t output[64]);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_CHACHA20_H */

BIN
nostr_core/nostr_chacha20.o Normal file

Binary file not shown.

744
nostr_core/nostr_core.h Normal file
View File

@@ -0,0 +1,744 @@
/*
* NOSTR Core Library
*
* A C library for NOSTR protocol implementation
* Self-contained crypto implementation (no external crypto dependencies)
*
* Features:
* - BIP39 mnemonic generation and validation
* - BIP32 hierarchical deterministic key derivation (NIP-06 compliant)
* - NOSTR key pair generation and management
* - Event creation, signing, and serialization
* - Relay communication (websocket-based)
* - Identity management and persistence
*/
#ifndef NOSTR_CORE_H
#define NOSTR_CORE_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
// Forward declare cJSON to avoid requiring cJSON.h in public header
typedef struct cJSON cJSON;
// Return codes
#define NOSTR_SUCCESS 0
#define NOSTR_ERROR_INVALID_INPUT -1
#define NOSTR_ERROR_CRYPTO_FAILED -2
#define NOSTR_ERROR_MEMORY_FAILED -3
#define NOSTR_ERROR_IO_FAILED -4
#define NOSTR_ERROR_NETWORK_FAILED -5
#define NOSTR_ERROR_NIP04_INVALID_FORMAT -10
#define NOSTR_ERROR_NIP04_DECRYPT_FAILED -11
#define NOSTR_ERROR_NIP04_BUFFER_TOO_SMALL -12
#define NOSTR_ERROR_NIP44_INVALID_FORMAT -13
#define NOSTR_ERROR_NIP44_DECRYPT_FAILED -14
#define NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL -15
// Debug control - uncomment to enable debug output
// #define NOSTR_DEBUG_ENABLED
// Constants
#define NOSTR_PRIVATE_KEY_SIZE 32
#define NOSTR_PUBLIC_KEY_SIZE 32
#define NOSTR_HEX_KEY_SIZE 65 // 64 + null terminator
#define NOSTR_BECH32_KEY_SIZE 100
#define NOSTR_MAX_CONTENT_SIZE 2048
#define NOSTR_MAX_URL_SIZE 256
// NIP-04 Constants
#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB
#define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
// Input type detection
typedef enum {
NOSTR_INPUT_UNKNOWN = 0,
NOSTR_INPUT_MNEMONIC,
NOSTR_INPUT_NSEC_HEX,
NOSTR_INPUT_NSEC_BECH32
} nostr_input_type_t;
// Relay permissions
typedef enum {
NOSTR_RELAY_READ_WRITE = 0,
NOSTR_RELAY_READ_ONLY,
NOSTR_RELAY_WRITE_ONLY
} nostr_relay_permission_t;
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// LIBRARY MAINTENANCE - KEEP THE SHELVES NICE AND ORGANIZED.
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Initialize the NOSTR core library (must be called before using other functions)
*
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_init(void);
/**
* Cleanup the NOSTR core library (call when done)
*/
void nostr_cleanup(void);
/**
* Get human-readable error message for error code
*
* @param error_code Error code from other functions
* @return Human-readable error string
*/
const char* nostr_strerror(int error_code);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// GENERAL NOSTR UTILITIES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Convert bytes to hexadecimal string
*
* @param bytes Input bytes
* @param len Number of bytes
* @param hex Output hex string (must be at least len*2+1 bytes)
*/
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex);
/**
* Convert hexadecimal string to bytes
*
* @param hex Input hex string
* @param bytes Output bytes buffer
* @param len Expected number of bytes
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len);
/**
* Generate public key from private key
*
* @param private_key Input private key (32 bytes)
* @param public_key Output public key (32 bytes, x-only)
* @return 0 on success, non-zero on failure
*/
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
/**
* Sign a hash using BIP-340 Schnorr signatures (NOSTR standard)
*
* @param private_key Input private key (32 bytes)
* @param hash Input hash to sign (32 bytes)
* @param signature Output signature (64 bytes)
* @return 0 on success, non-zero on failure
*/
int nostr_schnorr_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-01: BASIC PROTOCOL FLOW
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Create and sign a NOSTR event
*
* @param kind Event kind (0=profile, 1=text, 3=contacts, 10002=relays, etc.)
* @param content Event content string
* @param tags cJSON array of tags (NULL for empty tags)
* @param private_key Private key for signing (32 bytes)
* @param timestamp Event timestamp (0 for current time)
* @return cJSON event object (caller must free), NULL on failure
*/
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-04: ENCRYPTED DIRECT MESSAGES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Encrypt a message using NIP-04 (ECDH + AES-CBC + Base64)
*
* @param sender_private_key Sender's 32-byte private key
* @param recipient_public_key Recipient's 32-byte public key (x-only)
* @param plaintext Message to encrypt
* @param output Buffer for encrypted result (recommend NOSTR_NIP04_MAX_ENCRYPTED_SIZE)
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
/**
* Decrypt a NIP-04 encrypted message
*
* @param recipient_private_key Recipient's 32-byte private key
* @param sender_public_key Sender's 32-byte public key (x-only)
* @param encrypted_data Encrypted message in format "ciphertext?iv=iv"
* @param output Buffer for decrypted plaintext (recommend NOSTR_NIP04_MAX_PLAINTEXT_SIZE)
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-44: VERSIONED ENCRYPTED DIRECT MESSAGES
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Encrypt a message using NIP-44 v2 (ECDH + ChaCha20 + HMAC)
*
* @param sender_private_key Sender's 32-byte private key
* @param recipient_public_key Recipient's 32-byte public key (x-only)
* @param plaintext Message to encrypt
* @param output Buffer for encrypted result (recommend NOSTR_NIP44_MAX_PLAINTEXT_SIZE * 2)
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
/**
* Decrypt a NIP-44 encrypted message
*
* @param recipient_private_key Recipient's 32-byte private key
* @param sender_public_key Sender's 32-byte public key (x-only)
* @param encrypted_data Base64-encoded encrypted message
* @param output Buffer for decrypted plaintext
* @param output_size Size of output buffer
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-06: KEY DERIVATION FROM MNEMONIC
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Generate a random NOSTR keypair using cryptographically secure entropy
*
* @param private_key Output buffer for private key (32 bytes)
* @param public_key Output buffer for public key (32 bytes, x-only)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key);
/**
* Generate a BIP39 mnemonic phrase and derive NOSTR keys
*
* @param mnemonic Output buffer for mnemonic (at least 256 bytes recommended)
* @param mnemonic_size Size of mnemonic buffer
* @param account Account number for key derivation (default: 0)
* @param private_key Output buffer for private key (32 bytes)
* @param public_key Output buffer for public key (32 bytes)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
int account, unsigned char* private_key,
unsigned char* public_key);
/**
* Derive NOSTR keys from existing BIP39 mnemonic (NIP-06 compliant)
*
* @param mnemonic BIP39 mnemonic phrase
* @param account Account number for derivation path m/44'/1237'/account'/0/0
* @param private_key Output buffer for private key (32 bytes)
* @param public_key Output buffer for public key (32 bytes)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_derive_keys_from_mnemonic(const char* mnemonic, int account,
unsigned char* private_key, unsigned char* public_key);
/**
* Convert NOSTR key to bech32 format (nsec/npub)
*
* @param key Key data (32 bytes)
* @param hrp Human readable part ("nsec" or "npub")
* @param output Output buffer (at least 100 bytes recommended)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_key_to_bech32(const unsigned char* key, const char* hrp, char* output);
/**
* Detect the type of input string (mnemonic, hex nsec, bech32 nsec)
*
* @param input Input string to analyze
* @return Input type enum
*/
nostr_input_type_t nostr_detect_input_type(const char* input);
/**
* Validate and decode an nsec (hex or bech32) to private key
*
* @param input Input nsec string
* @param private_key Output buffer for private key (32 bytes)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_decode_nsec(const char* input, unsigned char* private_key);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// NIP-13: PROOF OF WORK
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* Add NIP-13 Proof of Work to an existing event
*
* @param event cJSON event object to add PoW to
* @param private_key Private key for re-signing the event during mining
* @param target_difficulty Target number of leading zero bits (default: 2 if 0)
* @param progress_callback Optional callback for progress updates (nonce, difficulty, user_data)
* @param user_data User data passed to progress callback
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty,
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
void* user_data);
// =============================================================================
// RELAY COMMUNICATION
// =============================================================================
/**
* Query a relay for a specific event
*
* @param relay_url Relay WebSocket URL (ws:// or wss://)
* @param pubkey_hex Author's public key in hex format
* @param kind Event kind to search for
* @return cJSON event object (caller must free), NULL if not found/error
*/
cJSON* nostr_query_relay_for_event(const char* relay_url, const char* pubkey_hex, int kind);
// =============================================================================
// SYNCHRONOUS MULTI-RELAY QUERIES AND PUBLISHING
// =============================================================================
// Query mode enum
typedef enum {
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is found
RELAY_QUERY_MOST_RECENT, // Wait for all relays, return most recent event
RELAY_QUERY_ALL_RESULTS // Wait for all relays, return all unique events
} relay_query_mode_t;
// Progress callback type for relay queries
typedef void (*relay_progress_callback_t)(
const char* relay_url, // Which relay is reporting (NULL for summary)
const char* status, // Status: "connecting", "subscribed", "event_found", "eose", "complete", "timeout", "error", "first_result", "all_complete"
const char* event_id, // Event ID when applicable (NULL otherwise)
int events_received, // Number of events from this relay
int total_relays, // Total number of relays
int completed_relays, // Number of relays finished
void* user_data // User data pointer
);
/**
* Query multiple relays synchronously with progress callbacks
*
* @param relay_urls Array of relay WebSocket URLs
* @param relay_count Number of relays in array
* @param filter cJSON filter object for query
* @param mode Query mode (FIRST_RESULT, MOST_RECENT, or ALL_RESULTS)
* @param result_count OUTPUT: number of events returned
* @param relay_timeout_seconds Timeout per relay in seconds (default: 2 if <= 0)
* @param callback Progress callback function (can be NULL)
* @param user_data User data passed to callback
* @return Array of cJSON events (caller must free each event and array), NULL on failure
*/
cJSON** synchronous_query_relays_with_progress(
const char** relay_urls,
int relay_count,
cJSON* filter,
relay_query_mode_t mode,
int* result_count,
int relay_timeout_seconds,
relay_progress_callback_t callback,
void* user_data
);
// Publish result enum
typedef enum {
PUBLISH_SUCCESS, // Event accepted by relay (received OK with true)
PUBLISH_REJECTED, // Event rejected by relay (received OK with false)
PUBLISH_TIMEOUT, // No response from relay within timeout
PUBLISH_ERROR // Connection error or other failure
} publish_result_t;
// Progress callback type for publishing
typedef void (*publish_progress_callback_t)(
const char* relay_url, // Which relay is reporting
const char* status, // Status: "connecting", "publishing", "accepted", "rejected", "timeout", "error"
const char* message, // OK message from relay (for rejected events)
int successful_publishes, // Count of successful publishes so far
int total_relays, // Total number of relays
int completed_relays, // Number of relays finished
void* user_data // User data pointer
);
/**
* Publish event to multiple relays synchronously with progress callbacks
*
* @param relay_urls Array of relay WebSocket URLs
* @param relay_count Number of relays in array
* @param event cJSON event object to publish
* @param success_count OUTPUT: number of successful publishes
* @param relay_timeout_seconds Timeout per relay in seconds (default: 5 if <= 0)
* @param callback Progress callback function (can be NULL)
* @param user_data User data passed to callback
* @return Array of publish_result_t (caller must free), NULL on failure
*/
publish_result_t* synchronous_publish_event_with_progress(
const char** relay_urls,
int relay_count,
cJSON* event,
int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback,
void* user_data
);
// =============================================================================
// RELAY POOL MANAGEMENT
// =============================================================================
// Forward declarations for relay pool types
typedef struct nostr_relay_pool nostr_relay_pool_t;
typedef struct nostr_pool_subscription nostr_pool_subscription_t;
// Pool connection status
typedef enum {
NOSTR_POOL_RELAY_DISCONNECTED = 0,
NOSTR_POOL_RELAY_CONNECTING = 1,
NOSTR_POOL_RELAY_CONNECTED = 2,
NOSTR_POOL_RELAY_ERROR = -1
} nostr_pool_relay_status_t;
// Relay statistics structure
typedef struct {
// Event counters
int events_received;
int events_published;
int events_published_ok;
int events_published_failed;
// Connection stats
int connection_attempts;
int connection_failures;
time_t connection_uptime_start;
time_t last_event_time;
// Latency measurements (milliseconds)
// NOTE: ping_latency_* values will be 0.0/-1.0 until PONG response handling is fixed
double ping_latency_current;
double ping_latency_avg;
double ping_latency_min;
double ping_latency_max;
double publish_latency_avg; // EVENT->OK response time
double query_latency_avg; // REQ->first EVENT response time
double query_latency_min; // Min query latency
double query_latency_max; // Max query latency
// Sample counts for averaging
int ping_samples;
int publish_samples;
int query_samples;
} nostr_relay_stats_t;
/**
* Create a new relay pool for managing multiple relay connections
*
* @return New relay pool instance (caller must destroy), NULL on failure
*/
nostr_relay_pool_t* nostr_relay_pool_create(void);
/**
* Add a relay to the pool
*
* @param pool Relay pool instance
* @param relay_url Relay WebSocket URL (ws:// or wss://)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
/**
* Remove a relay from the pool
*
* @param pool Relay pool instance
* @param relay_url Relay URL to remove
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
/**
* Destroy relay pool and cleanup all connections
*
* @param pool Relay pool instance to destroy
*/
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
/**
* Subscribe to events from multiple relays with event deduplication
*
* @param pool Relay pool instance
* @param relay_urls Array of relay URLs to subscribe to
* @param relay_count Number of relays in array
* @param filter cJSON filter object for subscription
* @param on_event Callback for received events (event, relay_url, user_data)
* @param on_eose Callback when all relays have sent EOSE (user_data)
* @param user_data User data passed to callbacks
* @return Subscription handle (caller must close), NULL on failure
*/
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data
);
/**
* Close a pool subscription
*
* @param subscription Subscription to close
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
/**
* Query multiple relays synchronously and return all matching events
*
* @param pool Relay pool instance
* @param relay_urls Array of relay URLs to query
* @param relay_count Number of relays in array
* @param filter cJSON filter object for query
* @param event_count Output: number of events returned
* @param timeout_ms Timeout in milliseconds
* @return Array of cJSON events (caller must free), NULL on failure/timeout
*/
cJSON** nostr_relay_pool_query_sync(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int* event_count,
int timeout_ms
);
/**
* Get a single event from multiple relays (returns the most recent one)
*
* @param pool Relay pool instance
* @param relay_urls Array of relay URLs to query
* @param relay_count Number of relays in array
* @param filter cJSON filter object for query
* @param timeout_ms Timeout in milliseconds
* @return cJSON event (caller must free), NULL if not found/timeout
*/
cJSON* nostr_relay_pool_get_event(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int timeout_ms
);
/**
* Publish an event to multiple relays
*
* @param pool Relay pool instance
* @param relay_urls Array of relay URLs to publish to
* @param relay_count Number of relays in array
* @param event cJSON event to publish
* @return Number of successful publishes, negative on error
*/
int nostr_relay_pool_publish(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* event
);
/**
* Get connection status for a relay in the pool
*
* @param pool Relay pool instance
* @param relay_url Relay URL to check
* @return Connection status enum
*/
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
nostr_relay_pool_t* pool,
const char* relay_url
);
/**
* Get list of all relays in pool with their status
*
* @param pool Relay pool instance
* @param relay_urls Output: array of relay URL strings (caller must free)
* @param statuses Output: array of status values (caller must free)
* @return Number of relays, negative on error
*/
int nostr_relay_pool_list_relays(
nostr_relay_pool_t* pool,
char*** relay_urls,
nostr_pool_relay_status_t** statuses
);
/**
* Run continuous event processing for active subscriptions (blocking)
* Processes incoming events and calls subscription callbacks until timeout or stopped
*
* @param pool Relay pool instance
* @param timeout_ms Timeout in milliseconds (0 = no timeout, runs indefinitely)
* @return Total number of events processed, negative on error
*/
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
/**
* Process events for active subscriptions (non-blocking, single pass)
* Processes available events and returns immediately
*
* @param pool Relay pool instance
* @param timeout_ms Maximum time to spend processing in milliseconds
* @return Number of events processed in this call, negative on error
*/
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
// =============================================================================
// RELAY POOL STATISTICS AND LATENCY
// =============================================================================
/**
* Get statistics for a specific relay in the pool
*
* @param pool Relay pool instance
* @param relay_url Relay URL to get statistics for
* @return Pointer to statistics structure (owned by pool), NULL if relay not found
*/
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url
);
/**
* Reset statistics for a specific relay
*
* @param pool Relay pool instance
* @param relay_url Relay URL to reset statistics for
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_relay_pool_reset_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url
);
/**
* Get current ping latency for a relay (most recent ping result)
*
* @param pool Relay pool instance
* @param relay_url Relay URL to check
* @return Ping latency in milliseconds, -1.0 if no ping data available
*/
double nostr_relay_pool_get_relay_ping_latency(
nostr_relay_pool_t* pool,
const char* relay_url
);
/**
* Get average query latency for a relay (REQ->first EVENT response time)
*
* @param pool Relay pool instance
* @param relay_url Relay URL to check
* @return Average query latency in milliseconds, -1.0 if no data available
*/
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
const char* relay_url
);
/**
* Manually trigger ping measurement for a relay (asynchronous)
*
* NOTE: PING frames are sent correctly, but PONG response handling needs debugging.
* Currently times out waiting for PONG responses. Future fix needed.
*
* @param pool Relay pool instance
* @param relay_url Relay URL to ping
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_relay_pool_ping_relay(
nostr_relay_pool_t* pool,
const char* relay_url
);
/**
* Manually trigger ping measurement for a relay and wait for response (synchronous)
*
* NOTE: PING frames are sent correctly, but PONG response handling needs debugging.
* Currently times out waiting for PONG responses. Future fix needed.
*
* @param pool Relay pool instance
* @param relay_url Relay URL to ping
* @param timeout_seconds Timeout in seconds (0 for default 5 seconds)
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_relay_pool_ping_relay_sync(
nostr_relay_pool_t* pool,
const char* relay_url,
int timeout_seconds
);
#endif // NOSTR_CORE_H

2142
nostr_core/nostr_crypto.c Normal file

File diff suppressed because it is too large Load Diff

186
nostr_core/nostr_crypto.h Normal file
View File

@@ -0,0 +1,186 @@
/*
* NOSTR Crypto - Self-contained cryptographic functions
*
* Embedded implementations of crypto primitives needed for NOSTR
* No external dependencies except standard C library
*/
#ifndef NOSTR_CRYPTO_H
#define NOSTR_CRYPTO_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// =============================================================================
// CORE CRYPTO FUNCTIONS
// =============================================================================
// Initialize crypto subsystem
int nostr_crypto_init(void);
// Cleanup crypto subsystem
void nostr_crypto_cleanup(void);
// SHA-256 hash function
int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash);
// HMAC-SHA256
int nostr_hmac_sha256(const unsigned char* key, size_t key_len,
const unsigned char* data, size_t data_len,
unsigned char* output);
// HMAC-SHA512
int nostr_hmac_sha512(const unsigned char* key, size_t key_len,
const unsigned char* data, size_t data_len,
unsigned char* output);
// PBKDF2 with HMAC-SHA512
int nostr_pbkdf2_hmac_sha512(const unsigned char* password, size_t password_len,
const unsigned char* salt, size_t salt_len,
int iterations,
unsigned char* output, size_t output_len);
// SHA-512 implementation (for testing)
int nostr_sha512(const unsigned char* data, size_t len, unsigned char* hash);
// =============================================================================
// SECP256K1 ELLIPTIC CURVE FUNCTIONS
// =============================================================================
// Verify private key is valid
int nostr_ec_private_key_verify(const unsigned char* private_key);
// Generate public key from private key
int nostr_ec_public_key_from_private_key(const unsigned char* private_key,
unsigned char* public_key);
// Sign data with ECDSA
int nostr_ec_sign(const unsigned char* private_key,
const unsigned char* hash,
unsigned char* signature);
// RFC 6979 deterministic nonce generation
int nostr_rfc6979_generate_k(const unsigned char* private_key,
const unsigned char* message_hash,
unsigned char* k_out);
// =============================================================================
// HKDF KEY DERIVATION FUNCTIONS
// =============================================================================
// HKDF Extract step
int nostr_hkdf_extract(const unsigned char* salt, size_t salt_len,
const unsigned char* ikm, size_t ikm_len,
unsigned char* prk);
// HKDF Expand step
int nostr_hkdf_expand(const unsigned char* prk, size_t prk_len,
const unsigned char* info, size_t info_len,
unsigned char* okm, size_t okm_len);
// HKDF (Extract + Expand)
int nostr_hkdf(const unsigned char* salt, size_t salt_len,
const unsigned char* ikm, size_t ikm_len,
const unsigned char* info, size_t info_len,
unsigned char* okm, size_t okm_len);
// ECDH shared secret computation (for debugging)
int ecdh_shared_secret(const unsigned char* private_key,
const unsigned char* public_key_x,
unsigned char* shared_secret);
// Base64 encoding function (for debugging)
size_t base64_encode(const unsigned char* data, size_t len, char* output, size_t output_size);
// =============================================================================
// NIP-04 AND NIP-44 ENCRYPTION FUNCTIONS
// =============================================================================
// Note: NOSTR_NIP04_MAX_PLAINTEXT_SIZE already defined in nostr_core.h
#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65536
// NIP-04 encryption (AES-256-CBC)
int nostr_nip04_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
// NIP-04 decryption
int nostr_nip04_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
// NIP-44 encryption (ChaCha20-Poly1305)
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
// NIP-44 encryption with fixed nonce (for testing)
int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
const unsigned char* nonce,
char* output,
size_t output_size);
// NIP-44 decryption
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
// =============================================================================
// BIP39 MNEMONIC FUNCTIONS
// =============================================================================
// Generate mnemonic from entropy
int nostr_bip39_mnemonic_from_bytes(const unsigned char* entropy, size_t entropy_len,
char* mnemonic);
// Validate mnemonic
int nostr_bip39_mnemonic_validate(const char* mnemonic);
// Convert mnemonic to seed
int nostr_bip39_mnemonic_to_seed(const char* mnemonic, const char* passphrase,
unsigned char* seed, size_t seed_len);
// =============================================================================
// BIP32 HD WALLET FUNCTIONS
// =============================================================================
typedef struct {
unsigned char private_key[32];
unsigned char public_key[33];
unsigned char chain_code[32];
uint32_t depth;
uint32_t parent_fingerprint;
uint32_t child_number;
} nostr_hd_key_t;
// Create master key from seed
int nostr_bip32_key_from_seed(const unsigned char* seed, size_t seed_len,
nostr_hd_key_t* master_key);
// Derive child key from parent
int nostr_bip32_derive_child(const nostr_hd_key_t* parent_key, uint32_t child_number,
nostr_hd_key_t* child_key);
// Derive key from path
int nostr_bip32_derive_path(const nostr_hd_key_t* master_key, const uint32_t* path,
size_t path_len, nostr_hd_key_t* derived_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_CRYPTO_H

BIN
nostr_core/nostr_crypto.o Normal file

Binary file not shown.

View File

@@ -0,0 +1,238 @@
#include "nostr_secp256k1.h"
#include <secp256k1.h>
#include <secp256k1_schnorrsig.h>
#include <secp256k1_ecdh.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
// Global context for secp256k1 operations
static secp256k1_context* g_ctx = NULL;
int nostr_secp256k1_context_create(void) {
if (g_ctx != NULL) {
return 1; // Already initialized
}
g_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
if (g_ctx == NULL) {
return 0;
}
// Add some randomization to the context for better security
unsigned char randomize[32];
// In a real implementation, you'd want better randomness
// For now, just use a simple pattern
for (int i = 0; i < 32; i++) {
randomize[i] = (unsigned char)(i * 7 + 13);
}
if (!secp256k1_context_randomize(g_ctx, randomize)) {
secp256k1_context_destroy(g_ctx);
g_ctx = NULL;
return 0;
}
return 1;
}
void nostr_secp256k1_context_destroy(void) {
if (g_ctx != NULL) {
secp256k1_context_destroy(g_ctx);
g_ctx = NULL;
}
}
int nostr_secp256k1_ec_seckey_verify(const unsigned char *seckey) {
if (g_ctx == NULL || seckey == NULL) {
return 0;
}
return secp256k1_ec_seckey_verify(g_ctx, seckey);
}
int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey) {
if (g_ctx == NULL || pubkey == NULL || seckey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
if (!secp256k1_ec_pubkey_create(g_ctx, &internal_pubkey, seckey)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_keypair_create(nostr_secp256k1_keypair *keypair, const unsigned char *seckey) {
if (g_ctx == NULL || keypair == NULL || seckey == NULL) {
return 0;
}
secp256k1_keypair internal_keypair;
if (!secp256k1_keypair_create(g_ctx, &internal_keypair, seckey)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(keypair->data, &internal_keypair, sizeof(secp256k1_keypair));
return 1;
}
int nostr_secp256k1_keypair_xonly_pub(nostr_secp256k1_xonly_pubkey *pubkey, const nostr_secp256k1_keypair *keypair) {
if (g_ctx == NULL || pubkey == NULL || keypair == NULL) {
return 0;
}
secp256k1_keypair internal_keypair;
secp256k1_xonly_pubkey internal_xonly;
// Copy from our wrapper to internal representation
memcpy(&internal_keypair, keypair->data, sizeof(secp256k1_keypair));
if (!secp256k1_keypair_xonly_pub(g_ctx, &internal_xonly, NULL, &internal_keypair)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_xonly, sizeof(secp256k1_xonly_pubkey));
return 1;
}
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey *pubkey, const unsigned char *input32) {
if (g_ctx == NULL || pubkey == NULL || input32 == NULL) {
return 0;
}
secp256k1_xonly_pubkey internal_xonly;
if (!secp256k1_xonly_pubkey_parse(g_ctx, &internal_xonly, input32)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_xonly, sizeof(secp256k1_xonly_pubkey));
return 1;
}
int nostr_secp256k1_xonly_pubkey_serialize(unsigned char *output32, const nostr_secp256k1_xonly_pubkey *pubkey) {
if (g_ctx == NULL || output32 == NULL || pubkey == NULL) {
return 0;
}
secp256k1_xonly_pubkey internal_xonly;
// Copy from our wrapper to internal representation
memcpy(&internal_xonly, pubkey->data, sizeof(secp256k1_xonly_pubkey));
return secp256k1_xonly_pubkey_serialize(g_ctx, output32, &internal_xonly);
}
int nostr_secp256k1_schnorrsig_sign32(unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_keypair *keypair, const unsigned char *aux_rand32) {
if (g_ctx == NULL || sig64 == NULL || msghash32 == NULL || keypair == NULL) {
return 0;
}
secp256k1_keypair internal_keypair;
// Copy from our wrapper to internal representation
memcpy(&internal_keypair, keypair->data, sizeof(secp256k1_keypair));
return secp256k1_schnorrsig_sign32(g_ctx, sig64, msghash32, &internal_keypair, aux_rand32);
}
int nostr_secp256k1_schnorrsig_verify(const unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_xonly_pubkey *pubkey) {
if (g_ctx == NULL || sig64 == NULL || msghash32 == NULL || pubkey == NULL) {
return 0;
}
secp256k1_xonly_pubkey internal_xonly;
// Copy from our wrapper to internal representation
memcpy(&internal_xonly, pubkey->data, sizeof(secp256k1_xonly_pubkey));
return secp256k1_schnorrsig_verify(g_ctx, sig64, msghash32, 32, &internal_xonly);
}
int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char *output, const nostr_secp256k1_pubkey *pubkey) {
if (g_ctx == NULL || output == NULL || pubkey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
size_t outputlen = 33;
// Copy from our wrapper to internal representation
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
return secp256k1_ec_pubkey_serialize(g_ctx, output, &outputlen, &internal_pubkey, SECP256K1_EC_COMPRESSED);
}
int nostr_secp256k1_ec_seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak) {
if (g_ctx == NULL || seckey == NULL || tweak == NULL) {
return 0;
}
return secp256k1_ec_seckey_tweak_add(g_ctx, seckey, tweak);
}
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen) {
if (g_ctx == NULL || pubkey == NULL || input == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
if (!secp256k1_ec_pubkey_parse(g_ctx, &internal_pubkey, input, inputlen)) {
return 0;
}
// Copy the internal representation to our wrapper
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey, void *hashfp, void *data) {
if (g_ctx == NULL || result == NULL || pubkey == NULL || seckey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
// Copy from our wrapper to internal representation
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
return secp256k1_ecdh(g_ctx, result, &internal_pubkey, seckey, hashfp, data);
}
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) {
if (buf == NULL || len == 0) {
return 0;
}
// Try to use /dev/urandom for good randomness
int fd = open("/dev/urandom", O_RDONLY);
if (fd >= 0) {
ssize_t result = read(fd, buf, len);
close(fd);
if (result == (ssize_t)len) {
return 1;
}
}
// Fallback to a simple PRNG (not cryptographically secure, but better than nothing)
// In a real implementation, you'd want to use a proper CSPRNG
static unsigned long seed = 1;
for (size_t i = 0; i < len; i++) {
seed = seed * 1103515245 + 12345;
buf[i] = (unsigned char)(seed >> 16);
}
return 1;
}

View File

@@ -0,0 +1,141 @@
#ifndef NOSTR_SECP256K1_H
#define NOSTR_SECP256K1_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
/** Opaque data structure that holds a parsed and valid public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_pubkey {
unsigned char data[64];
} nostr_secp256k1_pubkey;
/** Opaque data structure that holds a parsed keypair.
* Guaranteed to be 96 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_keypair {
unsigned char data[96];
} nostr_secp256k1_keypair;
/** Opaque data structure that holds a parsed x-only public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_xonly_pubkey {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
/** Initialize the secp256k1 library. Must be called before any other functions.
* Returns: 1 on success, 0 on failure.
*/
int nostr_secp256k1_context_create(void);
/** Clean up the secp256k1 library resources.
*/
void nostr_secp256k1_context_destroy(void);
/** Verify an elliptic curve secret key.
* Returns: 1: secret key is valid, 0: secret key is invalid
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_ec_seckey_verify(const unsigned char *seckey);
/** Compute the public key for a secret key.
* Returns: 1: secret was valid, public key stored. 0: secret was invalid.
* Out: pubkey: pointer to the created public key.
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey);
/** Create a keypair from a secret key.
* Returns: 1: keypair created, 0: secret key invalid.
* Out: keypair: pointer to the created keypair.
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_keypair_create(nostr_secp256k1_keypair *keypair, const unsigned char *seckey);
/** Get the x-only public key from a keypair.
* Returns: 1 always.
* Out: pubkey: pointer to storage for the x-only public key.
* In: keypair: pointer to a keypair.
*/
int nostr_secp256k1_keypair_xonly_pub(nostr_secp256k1_xonly_pubkey *pubkey, const nostr_secp256k1_keypair *keypair);
/** Parse an x-only public key from bytes.
* Returns: 1: public key parsed, 0: invalid public key.
* Out: pubkey: pointer to the created x-only public key.
* In: input32: pointer to a 32-byte x-only public key.
*/
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey *pubkey, const unsigned char *input32);
/** Serialize an x-only public key to bytes.
* Returns: 1 always.
* Out: output32: pointer to a 32-byte array to store the serialized key.
* In: pubkey: pointer to an x-only public key.
*/
int nostr_secp256k1_xonly_pubkey_serialize(unsigned char *output32, const nostr_secp256k1_xonly_pubkey *pubkey);
/** Create a Schnorr signature.
* Returns: 1: signature created, 0: nonce generation failed or secret key invalid.
* Out: sig64: pointer to a 64-byte array where the signature will be placed.
* In: msghash32: the 32-byte message hash being signed.
* keypair: pointer to an initialized keypair.
* aux_rand32: pointer to 32 bytes of auxiliary randomness (can be NULL).
*/
int nostr_secp256k1_schnorrsig_sign32(unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_keypair *keypair, const unsigned char *aux_rand32);
/** Verify a Schnorr signature.
* Returns: 1: correct signature, 0: incorrect signature
* In: sig64: pointer to the 64-byte signature being verified.
* msghash32: the 32-byte message hash being verified.
* pubkey: pointer to an x-only public key to verify with.
*/
int nostr_secp256k1_schnorrsig_verify(const unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_xonly_pubkey *pubkey);
/** Serialize a pubkey object into a serialized byte sequence.
* Returns: 1 always.
* Out: output: pointer to a 33-byte array to place the serialized key in.
* In: pubkey: pointer to a secp256k1_pubkey containing an initialized public key.
*
* The output will be a 33-byte compressed public key (0x02 or 0x03 prefix + 32 bytes x coordinate).
*/
int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char *output, const nostr_secp256k1_pubkey *pubkey);
/** Tweak a secret key by adding a 32-byte tweak to it.
* Returns: 1: seckey was valid, 0: seckey invalid or resulting key invalid
* In/Out: seckey: pointer to a 32-byte secret key. Will be modified in-place.
* In: tweak: pointer to a 32-byte tweak.
*/
int nostr_secp256k1_ec_seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak);
/** Parse a variable-length public key into the pubkey object.
* Returns: 1: public key parsed, 0: invalid public key.
* Out: pubkey: pointer to the created public key.
* In: input: pointer to a serialized public key
* inputlen: length of the array pointed to by input
*/
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen);
/** Compute an EC Diffie-Hellman secret in constant time.
* Returns: 1: exponentiation was successful, 0: scalar was invalid (zero or overflow)
* Out: result: a 32-byte array which will be populated by an ECDH secret computed from point and scalar
* In: pubkey: a pointer to a secp256k1_pubkey containing an initialized public key
* seckey: a 32-byte scalar with which to multiply the point
*/
int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey, void *hashfp, void *data);
/** Generate cryptographically secure random bytes.
* Returns: 1: success, 0: failure
* Out: buf: buffer to fill with random bytes
* In: len: number of bytes to generate
*/
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_SECP256K1_H */

Binary file not shown.