/* * NIP-17: Private Direct Messages Implementation * https://github.com/nostr-protocol/nips/blob/master/17.md */ #define _GNU_SOURCE #include "nip017.h" #include "nip059.h" #include "nip001.h" #include "utils.h" #include "nostr_common.h" #include #include #include #include // Forward declarations for crypto functions int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key); /** * Create tags array for DM events */ static cJSON* create_dm_tags(const char** recipient_pubkeys, int num_recipients, const char* subject, const char* reply_to_event_id, const char* reply_relay_url) { cJSON* tags = cJSON_CreateArray(); if (!tags) return NULL; // Add "p" tags for each recipient for (int i = 0; i < num_recipients; i++) { cJSON* p_tag = cJSON_CreateArray(); if (!p_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkeys[i])); // Add relay URL if provided (recommended) if (reply_relay_url) { cJSON_AddItemToArray(p_tag, cJSON_CreateString(reply_relay_url)); } cJSON_AddItemToArray(tags, p_tag); } // Add subject tag if provided if (subject && strlen(subject) > 0) { cJSON* subject_tag = cJSON_CreateArray(); if (!subject_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(subject_tag, cJSON_CreateString("subject")); cJSON_AddItemToArray(subject_tag, cJSON_CreateString(subject)); cJSON_AddItemToArray(tags, subject_tag); } // Add reply reference if provided if (reply_to_event_id && strlen(reply_to_event_id) > 0) { cJSON* e_tag = cJSON_CreateArray(); if (!e_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(e_tag, cJSON_CreateString("e")); cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_to_event_id)); if (reply_relay_url) { cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_relay_url)); } // For replies, add "reply" marker as per NIP-17 cJSON_AddItemToArray(e_tag, cJSON_CreateString("reply")); cJSON_AddItemToArray(tags, e_tag); } return tags; } /** * NIP-17: Create a chat message event (kind 14) */ cJSON* nostr_nip17_create_chat_event(const char* message, const char** recipient_pubkeys, int num_recipients, const char* subject, const char* reply_to_event_id, const char* reply_relay_url, const char* sender_pubkey_hex) { if (!message || !recipient_pubkeys || num_recipients <= 0 || !sender_pubkey_hex) { return NULL; } // Create tags cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject, reply_to_event_id, reply_relay_url); if (!tags) { return NULL; } // Create the chat rumor (kind 14, unsigned) cJSON* chat_event = nostr_nip59_create_rumor(14, message, tags, sender_pubkey_hex, 0); cJSON_Delete(tags); // Tags are duplicated in create_rumor return chat_event; } /** * NIP-17: Create a file message event (kind 15) */ cJSON* nostr_nip17_create_file_event(const char* file_url, const char* file_type, const char* encryption_algorithm, const char* decryption_key, const char* decryption_nonce, const char* file_hash, const char* original_file_hash, size_t file_size, const char* dimensions, const char* blurhash, const char* thumbnail_url, const char** recipient_pubkeys, int num_recipients, const char* subject, const char* reply_to_event_id, const char* reply_relay_url, const char* sender_pubkey_hex) { if (!file_url || !file_type || !encryption_algorithm || !decryption_key || !decryption_nonce || !file_hash || !recipient_pubkeys || num_recipients <= 0 || !sender_pubkey_hex) { return NULL; } // Create base tags cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject, reply_to_event_id, reply_relay_url); if (!tags) { return NULL; } // Add file-specific tags cJSON* file_type_tag = cJSON_CreateArray(); cJSON_AddItemToArray(file_type_tag, cJSON_CreateString("file-type")); cJSON_AddItemToArray(file_type_tag, cJSON_CreateString(file_type)); cJSON_AddItemToArray(tags, file_type_tag); cJSON* encryption_tag = cJSON_CreateArray(); cJSON_AddItemToArray(encryption_tag, cJSON_CreateString("encryption-algorithm")); cJSON_AddItemToArray(encryption_tag, cJSON_CreateString(encryption_algorithm)); cJSON_AddItemToArray(tags, encryption_tag); cJSON* key_tag = cJSON_CreateArray(); cJSON_AddItemToArray(key_tag, cJSON_CreateString("decryption-key")); cJSON_AddItemToArray(key_tag, cJSON_CreateString(decryption_key)); cJSON_AddItemToArray(tags, key_tag); cJSON* nonce_tag = cJSON_CreateArray(); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("decryption-nonce")); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(decryption_nonce)); cJSON_AddItemToArray(tags, nonce_tag); cJSON* x_tag = cJSON_CreateArray(); cJSON_AddItemToArray(x_tag, cJSON_CreateString("x")); cJSON_AddItemToArray(x_tag, cJSON_CreateString(file_hash)); cJSON_AddItemToArray(tags, x_tag); // Optional tags if (original_file_hash && strlen(original_file_hash) > 0) { cJSON* ox_tag = cJSON_CreateArray(); cJSON_AddItemToArray(ox_tag, cJSON_CreateString("ox")); cJSON_AddItemToArray(ox_tag, cJSON_CreateString(original_file_hash)); cJSON_AddItemToArray(tags, ox_tag); } if (file_size > 0) { char size_str[32]; snprintf(size_str, sizeof(size_str), "%zu", file_size); cJSON* size_tag = cJSON_CreateArray(); cJSON_AddItemToArray(size_tag, cJSON_CreateString("size")); cJSON_AddItemToArray(size_tag, cJSON_CreateString(size_str)); cJSON_AddItemToArray(tags, size_tag); } if (dimensions && strlen(dimensions) > 0) { cJSON* dim_tag = cJSON_CreateArray(); cJSON_AddItemToArray(dim_tag, cJSON_CreateString("dim")); cJSON_AddItemToArray(dim_tag, cJSON_CreateString(dimensions)); cJSON_AddItemToArray(tags, dim_tag); } if (blurhash && strlen(blurhash) > 0) { cJSON* blurhash_tag = cJSON_CreateArray(); cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString("blurhash")); cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString(blurhash)); cJSON_AddItemToArray(tags, blurhash_tag); } if (thumbnail_url && strlen(thumbnail_url) > 0) { cJSON* thumb_tag = cJSON_CreateArray(); cJSON_AddItemToArray(thumb_tag, cJSON_CreateString("thumb")); cJSON_AddItemToArray(thumb_tag, cJSON_CreateString(thumbnail_url)); cJSON_AddItemToArray(tags, thumb_tag); } // Create the file rumor (kind 15, unsigned) cJSON* file_event = nostr_nip59_create_rumor(15, file_url, tags, sender_pubkey_hex, 0); cJSON_Delete(tags); // Tags are duplicated in create_rumor return file_event; } /** * NIP-17: Create a relay list event (kind 10050) */ cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls, int num_relays, const unsigned char* private_key) { if (!relay_urls || num_relays <= 0 || !private_key) { return NULL; } // Get public key unsigned char public_key[32]; if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) { return NULL; } char pubkey_hex[65]; nostr_bytes_to_hex(public_key, 32, pubkey_hex); // Create tags with relay URLs cJSON* tags = cJSON_CreateArray(); if (!tags) { return NULL; } for (int i = 0; i < num_relays; i++) { cJSON* relay_tag = cJSON_CreateArray(); if (!relay_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay")); cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_urls[i])); cJSON_AddItemToArray(tags, relay_tag); } // Create and sign the event cJSON* relay_event = nostr_create_and_sign_event(10050, "", tags, private_key, time(NULL)); cJSON_Delete(tags); // Tags are duplicated in create_and_sign_event return relay_event; } /** * NIP-17: Send a direct message to recipients */ int nostr_nip17_send_dm(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, const unsigned char* sender_private_key, cJSON** gift_wraps_out, int max_gift_wraps) { if (!dm_event || !recipient_pubkeys || num_recipients <= 0 || !sender_private_key || !gift_wraps_out || max_gift_wraps <= 0) { return -1; } int created_wraps = 0; for (int i = 0; i < num_recipients && created_wraps < max_gift_wraps; i++) { // Convert recipient pubkey hex to bytes unsigned char recipient_public_key[32]; if (nostr_hex_to_bytes(recipient_pubkeys[i], recipient_public_key, 32) != 0) { continue; // Skip invalid pubkeys } // Create seal for this recipient cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key); if (!seal) { continue; // Skip if sealing fails } // Create gift wrap for this recipient cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i]); cJSON_Delete(seal); // Seal is now wrapped if (!gift_wrap) { continue; // Skip if wrapping fails } gift_wraps_out[created_wraps++] = gift_wrap; } // Also create a gift wrap for the sender (so they can see their own messages) if (created_wraps < max_gift_wraps) { // Get sender's public key unsigned char sender_public_key[32]; if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) == 0) { char sender_pubkey_hex[65]; nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex); // Create seal for sender cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key); if (sender_seal) { // Create gift wrap for sender cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex); cJSON_Delete(sender_seal); if (sender_gift_wrap) { gift_wraps_out[created_wraps++] = sender_gift_wrap; } } } } return created_wraps; } /** * NIP-17: Receive and decrypt a direct message */ cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap, const unsigned char* recipient_private_key) { if (!gift_wrap || !recipient_private_key) { return NULL; } // Unwrap the gift wrap to get the seal cJSON* seal = nostr_nip59_unwrap_gift(gift_wrap, recipient_private_key); if (!seal) { return NULL; } // Get sender's public key from the seal cJSON* seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey"); if (!seal_pubkey_item || !cJSON_IsString(seal_pubkey_item)) { cJSON_Delete(seal); return NULL; } const char* sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item); // Convert sender pubkey hex to bytes unsigned char sender_public_key[32]; if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) { cJSON_Delete(seal); return NULL; } // Unseal the rumor cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key); cJSON_Delete(seal); // Seal is no longer needed return rumor; } /** * NIP-17: Extract DM relay URLs from a user's kind 10050 event */ int nostr_nip17_extract_dm_relays(cJSON* relay_list_event, char** relay_urls_out, int max_relays) { if (!relay_list_event || !relay_urls_out || max_relays <= 0) { return -1; } // Check if this is a kind 10050 event cJSON* kind_item = cJSON_GetObjectItem(relay_list_event, "kind"); if (!kind_item || !cJSON_IsNumber(kind_item) || cJSON_GetNumberValue(kind_item) != 10050) { return -1; } // Get tags array cJSON* tags_item = cJSON_GetObjectItem(relay_list_event, "tags"); if (!tags_item || !cJSON_IsArray(tags_item)) { return -1; } int extracted = 0; cJSON* tag_item; cJSON_ArrayForEach(tag_item, tags_item) { if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) { continue; } // Check if this is a "relay" tag cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0); cJSON* relay_url = cJSON_GetArrayItem(tag_item, 1); if (cJSON_IsString(tag_name) && cJSON_IsString(relay_url) && strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) { if (extracted < max_relays) { relay_urls_out[extracted] = strdup(cJSON_GetStringValue(relay_url)); if (relay_urls_out[extracted]) { extracted++; } } } } return extracted; }