406 lines
14 KiB
C
406 lines
14 KiB
C
/*
|
|
* NIP-17: Private Direct Messages Implementation
|
|
* https://github.com/nostr-protocol/nips/blob/master/17.md
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#include "nip017.h"
|
|
#include "nip059.h"
|
|
#include "nip001.h"
|
|
#include "utils.h"
|
|
#include "nostr_common.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
// Forward declarations for crypto functions
|
|
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
|
|
|
|
/**
|
|
* Create tags array for DM events
|
|
*/
|
|
static cJSON* create_dm_tags(const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const char* subject,
|
|
const char* reply_to_event_id,
|
|
const char* reply_relay_url) {
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) return NULL;
|
|
|
|
// Add "p" tags for each recipient
|
|
for (int i = 0; i < num_recipients; i++) {
|
|
cJSON* p_tag = cJSON_CreateArray();
|
|
if (!p_tag) {
|
|
cJSON_Delete(tags);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkeys[i]));
|
|
// Add relay URL if provided (recommended)
|
|
if (reply_relay_url) {
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString(reply_relay_url));
|
|
}
|
|
cJSON_AddItemToArray(tags, p_tag);
|
|
}
|
|
|
|
// Add subject tag if provided
|
|
if (subject && strlen(subject) > 0) {
|
|
cJSON* subject_tag = cJSON_CreateArray();
|
|
if (!subject_tag) {
|
|
cJSON_Delete(tags);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToArray(subject_tag, cJSON_CreateString("subject"));
|
|
cJSON_AddItemToArray(subject_tag, cJSON_CreateString(subject));
|
|
cJSON_AddItemToArray(tags, subject_tag);
|
|
}
|
|
|
|
// Add reply reference if provided
|
|
if (reply_to_event_id && strlen(reply_to_event_id) > 0) {
|
|
cJSON* e_tag = cJSON_CreateArray();
|
|
if (!e_tag) {
|
|
cJSON_Delete(tags);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
|
|
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_to_event_id));
|
|
if (reply_relay_url) {
|
|
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_relay_url));
|
|
}
|
|
// For replies, add "reply" marker as per NIP-17
|
|
cJSON_AddItemToArray(e_tag, cJSON_CreateString("reply"));
|
|
cJSON_AddItemToArray(tags, e_tag);
|
|
}
|
|
|
|
return tags;
|
|
}
|
|
|
|
/**
|
|
* NIP-17: Create a chat message event (kind 14)
|
|
*/
|
|
cJSON* nostr_nip17_create_chat_event(const char* message,
|
|
const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const char* subject,
|
|
const char* reply_to_event_id,
|
|
const char* reply_relay_url,
|
|
const char* sender_pubkey_hex) {
|
|
if (!message || !recipient_pubkeys || num_recipients <= 0 || !sender_pubkey_hex) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create tags
|
|
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
|
|
reply_to_event_id, reply_relay_url);
|
|
if (!tags) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create the chat rumor (kind 14, unsigned)
|
|
cJSON* chat_event = nostr_nip59_create_rumor(14, message, tags, sender_pubkey_hex, 0);
|
|
|
|
cJSON_Delete(tags); // Tags are duplicated in create_rumor
|
|
|
|
return chat_event;
|
|
}
|
|
|
|
/**
|
|
* NIP-17: Create a file message event (kind 15)
|
|
*/
|
|
cJSON* nostr_nip17_create_file_event(const char* file_url,
|
|
const char* file_type,
|
|
const char* encryption_algorithm,
|
|
const char* decryption_key,
|
|
const char* decryption_nonce,
|
|
const char* file_hash,
|
|
const char* original_file_hash,
|
|
size_t file_size,
|
|
const char* dimensions,
|
|
const char* blurhash,
|
|
const char* thumbnail_url,
|
|
const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const char* subject,
|
|
const char* reply_to_event_id,
|
|
const char* reply_relay_url,
|
|
const char* sender_pubkey_hex) {
|
|
if (!file_url || !file_type || !encryption_algorithm || !decryption_key ||
|
|
!decryption_nonce || !file_hash || !recipient_pubkeys ||
|
|
num_recipients <= 0 || !sender_pubkey_hex) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create base tags
|
|
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
|
|
reply_to_event_id, reply_relay_url);
|
|
if (!tags) {
|
|
return NULL;
|
|
}
|
|
|
|
// Add file-specific tags
|
|
cJSON* file_type_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString("file-type"));
|
|
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString(file_type));
|
|
cJSON_AddItemToArray(tags, file_type_tag);
|
|
|
|
cJSON* encryption_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString("encryption-algorithm"));
|
|
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString(encryption_algorithm));
|
|
cJSON_AddItemToArray(tags, encryption_tag);
|
|
|
|
cJSON* key_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(key_tag, cJSON_CreateString("decryption-key"));
|
|
cJSON_AddItemToArray(key_tag, cJSON_CreateString(decryption_key));
|
|
cJSON_AddItemToArray(tags, key_tag);
|
|
|
|
cJSON* nonce_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("decryption-nonce"));
|
|
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(decryption_nonce));
|
|
cJSON_AddItemToArray(tags, nonce_tag);
|
|
|
|
cJSON* x_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
|
|
cJSON_AddItemToArray(x_tag, cJSON_CreateString(file_hash));
|
|
cJSON_AddItemToArray(tags, x_tag);
|
|
|
|
// Optional tags
|
|
if (original_file_hash && strlen(original_file_hash) > 0) {
|
|
cJSON* ox_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(ox_tag, cJSON_CreateString("ox"));
|
|
cJSON_AddItemToArray(ox_tag, cJSON_CreateString(original_file_hash));
|
|
cJSON_AddItemToArray(tags, ox_tag);
|
|
}
|
|
|
|
if (file_size > 0) {
|
|
char size_str[32];
|
|
snprintf(size_str, sizeof(size_str), "%zu", file_size);
|
|
cJSON* size_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(size_tag, cJSON_CreateString("size"));
|
|
cJSON_AddItemToArray(size_tag, cJSON_CreateString(size_str));
|
|
cJSON_AddItemToArray(tags, size_tag);
|
|
}
|
|
|
|
if (dimensions && strlen(dimensions) > 0) {
|
|
cJSON* dim_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(dim_tag, cJSON_CreateString("dim"));
|
|
cJSON_AddItemToArray(dim_tag, cJSON_CreateString(dimensions));
|
|
cJSON_AddItemToArray(tags, dim_tag);
|
|
}
|
|
|
|
if (blurhash && strlen(blurhash) > 0) {
|
|
cJSON* blurhash_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString("blurhash"));
|
|
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString(blurhash));
|
|
cJSON_AddItemToArray(tags, blurhash_tag);
|
|
}
|
|
|
|
if (thumbnail_url && strlen(thumbnail_url) > 0) {
|
|
cJSON* thumb_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString("thumb"));
|
|
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString(thumbnail_url));
|
|
cJSON_AddItemToArray(tags, thumb_tag);
|
|
}
|
|
|
|
// Create the file rumor (kind 15, unsigned)
|
|
cJSON* file_event = nostr_nip59_create_rumor(15, file_url, tags, sender_pubkey_hex, 0);
|
|
|
|
cJSON_Delete(tags); // Tags are duplicated in create_rumor
|
|
|
|
return file_event;
|
|
}
|
|
|
|
/**
|
|
* NIP-17: Create a relay list event (kind 10050)
|
|
*/
|
|
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
|
|
int num_relays,
|
|
const unsigned char* private_key) {
|
|
if (!relay_urls || num_relays <= 0 || !private_key) {
|
|
return NULL;
|
|
}
|
|
|
|
// Get public key
|
|
unsigned char public_key[32];
|
|
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
|
|
return NULL;
|
|
}
|
|
|
|
char pubkey_hex[65];
|
|
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
|
|
|
|
// Create tags with relay URLs
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) {
|
|
return NULL;
|
|
}
|
|
|
|
for (int i = 0; i < num_relays; i++) {
|
|
cJSON* relay_tag = cJSON_CreateArray();
|
|
if (!relay_tag) {
|
|
cJSON_Delete(tags);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
|
|
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_urls[i]));
|
|
cJSON_AddItemToArray(tags, relay_tag);
|
|
}
|
|
|
|
// Create and sign the event
|
|
cJSON* relay_event = nostr_create_and_sign_event(10050, "", tags, private_key, time(NULL));
|
|
|
|
cJSON_Delete(tags); // Tags are duplicated in create_and_sign_event
|
|
|
|
return relay_event;
|
|
}
|
|
|
|
/**
|
|
* NIP-17: Send a direct message to recipients
|
|
*/
|
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
|
const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const unsigned char* sender_private_key,
|
|
cJSON** gift_wraps_out,
|
|
int max_gift_wraps) {
|
|
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;
|
|
} |