/* * NIP-59: Gift Wrap Implementation * https://github.com/nostr-protocol/nips/blob/master/59.md */ #include "nip059.h" #include "nip044.h" #include "nip001.h" #include "utils.h" #include "nostr_common.h" #include #include #include #include // Forward declarations for crypto functions int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len); int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key); int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature); // Memory clearing utility static void memory_clear(const void *p, size_t len) { if (p && len) { memset((void *)p, 0, len); } } /** * Create a random timestamp within 2 days in the past (as per NIP-59 spec) */ static time_t random_past_timestamp(void) { time_t now = time(NULL); // Random time up to 2 days (172800 seconds) in the past long random_offset = (long)(rand() % 172800); return now - random_offset; } /** * Generate a random private key for gift wrap */ static int generate_random_private_key(unsigned char* private_key) { return nostr_secp256k1_get_random_bytes(private_key, 32); } /** * Create event ID from event data (without signature) */ static int create_event_id(cJSON* event, char* event_id_hex) { if (!event || !event_id_hex) { return -1; } // Get event fields for serialization cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at"); cJSON* kind_item = cJSON_GetObjectItem(event, "kind"); cJSON* tags_item = cJSON_GetObjectItem(event, "tags"); cJSON* content_item = cJSON_GetObjectItem(event, "content"); if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) { return -1; } // Create serialization array: [0, pubkey, created_at, kind, tags, content] cJSON* serialize_array = cJSON_CreateArray(); if (!serialize_array) { return -1; } cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1)); char* serialize_string = cJSON_PrintUnformatted(serialize_array); cJSON_Delete(serialize_array); if (!serialize_string) { return -1; } // Hash the serialized event unsigned char event_hash[32]; if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) { free(serialize_string); return -1; } // Convert hash to hex nostr_bytes_to_hex(event_hash, 32, event_id_hex); free(serialize_string); return 0; } /** * NIP-59: Create a rumor (unsigned event) */ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags, const char* pubkey_hex, time_t created_at) { if (!pubkey_hex || !content) { return NULL; } // Use provided timestamp or random past timestamp time_t event_time = (created_at == 0) ? random_past_timestamp() : created_at; // Create event structure (without id and sig - that's what makes it a rumor) cJSON* rumor = cJSON_CreateObject(); if (!rumor) { return NULL; } cJSON_AddStringToObject(rumor, "pubkey", pubkey_hex); cJSON_AddNumberToObject(rumor, "created_at", (double)event_time); cJSON_AddNumberToObject(rumor, "kind", kind); // Add tags (copy provided tags or create empty array) if (tags) { cJSON_AddItemToObject(rumor, "tags", cJSON_Duplicate(tags, 1)); } else { cJSON_AddItemToObject(rumor, "tags", cJSON_CreateArray()); } cJSON_AddStringToObject(rumor, "content", content); // Calculate and add event ID char event_id[65]; if (create_event_id(rumor, event_id) != 0) { cJSON_Delete(rumor); return NULL; } cJSON_AddStringToObject(rumor, "id", event_id); return rumor; } /** * NIP-59: Create a seal (kind 13) wrapping a rumor */ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key, const unsigned char* recipient_public_key) { if (!rumor || !sender_private_key || !recipient_public_key) { return NULL; } // Serialize the rumor to JSON char* rumor_json = cJSON_PrintUnformatted(rumor); if (!rumor_json) { return NULL; } // Encrypt the rumor using NIP-44 char encrypted_content[4096]; // Should be large enough for most events int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key, rumor_json, encrypted_content, sizeof(encrypted_content)); free(rumor_json); if (encrypt_result != NOSTR_SUCCESS) { return NULL; } // Get sender's public key unsigned char sender_public_key[32]; if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) { return NULL; } char sender_pubkey_hex[65]; nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex); // Create seal event (kind 13) cJSON* seal = cJSON_CreateObject(); if (!seal) { return NULL; } time_t seal_time = random_past_timestamp(); cJSON_AddStringToObject(seal, "pubkey", sender_pubkey_hex); cJSON_AddNumberToObject(seal, "created_at", (double)seal_time); cJSON_AddNumberToObject(seal, "kind", 13); cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); // Empty tags array cJSON_AddStringToObject(seal, "content", encrypted_content); // Calculate event ID char event_id[65]; if (create_event_id(seal, event_id) != 0) { cJSON_Delete(seal); return NULL; } cJSON_AddStringToObject(seal, "id", event_id); // Sign the seal unsigned char event_hash[32]; if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) { cJSON_Delete(seal); return NULL; } unsigned char signature[64]; if (nostr_ec_sign(sender_private_key, event_hash, signature) != 0) { cJSON_Delete(seal); return NULL; } char sig_hex[129]; nostr_bytes_to_hex(signature, 64, sig_hex); cJSON_AddStringToObject(seal, "sig", sig_hex); return seal; } /** * NIP-59: Create a gift wrap (kind 1059) wrapping a seal */ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex) { if (!seal || !recipient_public_key_hex) { return NULL; } // Serialize the seal to JSON char* seal_json = cJSON_PrintUnformatted(seal); if (!seal_json) { return NULL; } // Generate random private key for gift wrap unsigned char random_private_key[32]; if (generate_random_private_key(random_private_key) != 1) { free(seal_json); return NULL; } // Get random public key unsigned char random_public_key[32]; if (nostr_ec_public_key_from_private_key(random_private_key, random_public_key) != 0) { memory_clear(random_private_key, 32); free(seal_json); return NULL; } char random_pubkey_hex[65]; nostr_bytes_to_hex(random_public_key, 32, random_pubkey_hex); // Convert recipient pubkey hex to bytes unsigned char recipient_public_key[32]; if (nostr_hex_to_bytes(recipient_public_key_hex, recipient_public_key, 32) != 0) { memory_clear(random_private_key, 32); free(seal_json); return NULL; } // Encrypt the seal using NIP-44 char encrypted_content[8192]; // Larger buffer for nested encryption int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key, seal_json, encrypted_content, sizeof(encrypted_content)); free(seal_json); if (encrypt_result != NOSTR_SUCCESS) { memory_clear(random_private_key, 32); return NULL; } // Create gift wrap event (kind 1059) cJSON* gift_wrap = cJSON_CreateObject(); if (!gift_wrap) { memory_clear(random_private_key, 32); return NULL; } time_t wrap_time = random_past_timestamp(); cJSON_AddStringToObject(gift_wrap, "pubkey", random_pubkey_hex); cJSON_AddNumberToObject(gift_wrap, "created_at", (double)wrap_time); cJSON_AddNumberToObject(gift_wrap, "kind", 1059); // Add p tag for recipient cJSON* tags = cJSON_CreateArray(); cJSON* p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_public_key_hex)); cJSON_AddItemToArray(tags, p_tag); cJSON_AddItemToObject(gift_wrap, "tags", tags); cJSON_AddStringToObject(gift_wrap, "content", encrypted_content); // Calculate event ID char event_id[65]; if (create_event_id(gift_wrap, event_id) != 0) { memory_clear(random_private_key, 32); cJSON_Delete(gift_wrap); return NULL; } cJSON_AddStringToObject(gift_wrap, "id", event_id); // Sign the gift wrap unsigned char event_hash[32]; if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) { memory_clear(random_private_key, 32); cJSON_Delete(gift_wrap); return NULL; } unsigned char signature[64]; if (nostr_ec_sign(random_private_key, event_hash, signature) != 0) { memory_clear(random_private_key, 32); cJSON_Delete(gift_wrap); return NULL; } char sig_hex[129]; nostr_bytes_to_hex(signature, 64, sig_hex); cJSON_AddStringToObject(gift_wrap, "sig", sig_hex); // Clear the random private key from memory memory_clear(random_private_key, 32); return gift_wrap; } /** * NIP-59: Unwrap a gift wrap to get the seal */ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key) { if (!gift_wrap || !recipient_private_key) { return NULL; } // Get the encrypted content cJSON* content_item = cJSON_GetObjectItem(gift_wrap, "content"); if (!content_item || !cJSON_IsString(content_item)) { return NULL; } const char* encrypted_content = cJSON_GetStringValue(content_item); // Get the sender's public key (gift wrap pubkey) cJSON* pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey"); if (!pubkey_item || !cJSON_IsString(pubkey_item)) { return NULL; } const char* sender_pubkey_hex = cJSON_GetStringValue(pubkey_item); // Convert sender pubkey hex to bytes unsigned char sender_public_key[32]; if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) { return NULL; } // Decrypt the content using NIP-44 char decrypted_json[8192]; int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key, encrypted_content, decrypted_json, sizeof(decrypted_json)); if (decrypt_result != NOSTR_SUCCESS) { return NULL; } // Parse the decrypted JSON as the seal event cJSON* seal = cJSON_Parse(decrypted_json); if (!seal) { return NULL; } return seal; } /** * NIP-59: Unseal a seal to get the rumor */ cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key, const unsigned char* recipient_private_key) { if (!seal || !sender_public_key || !recipient_private_key) { return NULL; } // Get the encrypted content cJSON* content_item = cJSON_GetObjectItem(seal, "content"); if (!content_item || !cJSON_IsString(content_item)) { return NULL; } const char* encrypted_content = cJSON_GetStringValue(content_item); // Decrypt the content using NIP-44 char decrypted_json[4096]; int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key, encrypted_content, decrypted_json, sizeof(decrypted_json)); if (decrypt_result != NOSTR_SUCCESS) { return NULL; } // Parse the decrypted JSON as the rumor event cJSON* rumor = cJSON_Parse(decrypted_json); if (!rumor) { return NULL; } return rumor; }