406 lines
12 KiB
C
406 lines
12 KiB
C
/*
|
|
* 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
// 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;
|
|
} |