/* * Ginxsom Admin Authentication Module * Handles Kind 23456/23457 admin events with NIP-44 encryption * Based on c-relay's dm_admin.c implementation */ #include "ginxsom.h" #include "../nostr_core_lib/nostr_core/nostr_common.h" #include "../nostr_core_lib/nostr_core/nip001.h" #include "../nostr_core_lib/nostr_core/nip044.h" #include "../nostr_core_lib/nostr_core/utils.h" #include #include #include #include #include #include // Forward declarations int get_blossom_private_key(char *seckey_out, size_t max_len); int validate_admin_pubkey(const char *pubkey); // Global variables for admin auth static char g_blossom_seckey[65] = ""; // Cached blossom server private key static int g_keys_loaded = 0; // Whether keys have been loaded // Load blossom server keys if not already loaded static int ensure_keys_loaded(void) { if (!g_keys_loaded) { if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) { fprintf(stderr, "ERROR: Cannot load blossom private key for admin auth\n"); return -1; } g_keys_loaded = 1; } return 0; } // Validate that an event is a Kind 23456 admin command event int is_admin_command_event(cJSON *event, const char *relay_pubkey) { if (!event || !relay_pubkey) { return 0; } // Check kind = 23456 (admin command) cJSON *kind = cJSON_GetObjectItem(event, "kind"); if (!cJSON_IsNumber(kind) || kind->valueint != 23456) { return 0; } // Check tags for 'p' tag with relay pubkey cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (!cJSON_IsArray(tags)) { return 0; } int found_p_tag = 0; cJSON *tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON *tag_name = cJSON_GetArrayItem(tag, 0); cJSON *tag_value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(tag_name) && strcmp(tag_name->valuestring, "p") == 0 && cJSON_IsString(tag_value) && strcmp(tag_value->valuestring, relay_pubkey) == 0) { found_p_tag = 1; break; } } } return found_p_tag; } // Validate admin event signature and pubkey int validate_admin_event(cJSON *event) { if (!event) { return 0; } // Get event fields cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); cJSON *sig = cJSON_GetObjectItem(event, "sig"); if (!cJSON_IsString(pubkey) || !cJSON_IsString(sig)) { fprintf(stderr, "AUTH: Invalid event format - missing pubkey or sig\n"); return 0; } // Check if pubkey matches configured admin pubkey if (!validate_admin_pubkey(pubkey->valuestring)) { fprintf(stderr, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring); return 0; } // TODO: Validate event signature using nostr_core_lib // For now, assume signature is valid if pubkey matches // In production, this should verify the signature cryptographically return 1; } // Decrypt NIP-44 encrypted admin command int decrypt_admin_command(cJSON *event, char **decrypted_command_out) { if (!event || !decrypted_command_out) { return -1; } // Ensure we have the relay private key if (ensure_keys_loaded() != 0) { return -1; } // Get admin pubkey from event cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!cJSON_IsString(admin_pubkey_json)) { fprintf(stderr, "AUTH: Missing or invalid pubkey in event\n"); return -1; } // Get encrypted content cJSON *content = cJSON_GetObjectItem(event, "content"); if (!cJSON_IsString(content)) { fprintf(stderr, "AUTH: Missing or invalid content in event\n"); return -1; } // Convert hex keys to bytes unsigned char blossom_private_key[32]; unsigned char admin_public_key[32]; if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) { fprintf(stderr, "AUTH: Failed to parse blossom private key\n"); return -1; } if (nostr_hex_to_bytes(admin_pubkey_json->valuestring, admin_public_key, 32) != 0) { fprintf(stderr, "AUTH: Failed to parse admin public key\n"); return -1; } // Allocate buffer for decrypted content char decrypted_buffer[8192]; // Decrypt using NIP-44 int result = nostr_nip44_decrypt( blossom_private_key, admin_public_key, content->valuestring, decrypted_buffer, sizeof(decrypted_buffer) ); if (result != NOSTR_SUCCESS) { fprintf(stderr, "AUTH: NIP-44 decryption failed with error code %d\n", result); return -1; } // Allocate and copy decrypted content *decrypted_command_out = malloc(strlen(decrypted_buffer) + 1); if (!*decrypted_command_out) { fprintf(stderr, "AUTH: Failed to allocate memory for decrypted content\n"); return -1; } strcpy(*decrypted_command_out, decrypted_buffer); return 0; } // Parse decrypted command array int parse_admin_command(const char *decrypted_content, char ***command_array_out, int *command_count_out) { if (!decrypted_content || !command_array_out || !command_count_out) { return -1; } // Parse the decrypted content as JSON array cJSON *content_json = cJSON_Parse(decrypted_content); if (!content_json) { fprintf(stderr, "AUTH: Failed to parse decrypted content as JSON\n"); return -1; } if (!cJSON_IsArray(content_json)) { fprintf(stderr, "AUTH: Decrypted content is not a JSON array\n"); cJSON_Delete(content_json); return -1; } int array_size = cJSON_GetArraySize(content_json); if (array_size < 1) { fprintf(stderr, "AUTH: Command array is empty\n"); cJSON_Delete(content_json); return -1; } // Allocate command array char **command_array = malloc(array_size * sizeof(char *)); if (!command_array) { fprintf(stderr, "AUTH: Failed to allocate command array\n"); cJSON_Delete(content_json); return -1; } // Parse each array element as string for (int i = 0; i < array_size; i++) { cJSON *item = cJSON_GetArrayItem(content_json, i); if (!cJSON_IsString(item)) { fprintf(stderr, "AUTH: Command array element %d is not a string\n", i); // Clean up allocated strings for (int j = 0; j < i; j++) { free(command_array[j]); } free(command_array); cJSON_Delete(content_json); return -1; } command_array[i] = malloc(strlen(item->valuestring) + 1); if (!command_array[i]) { fprintf(stderr, "AUTH: Failed to allocate command string\n"); // Clean up allocated strings for (int j = 0; j < i; j++) { free(command_array[j]); } free(command_array); cJSON_Delete(content_json); return -1; } strcpy(command_array[i], item->valuestring); if (!command_array[i]) { fprintf(stderr, "AUTH: Failed to duplicate command string\n"); // Clean up allocated strings for (int j = 0; j < i; j++) { free(command_array[j]); } free(command_array); cJSON_Delete(content_json); return -1; } } cJSON_Delete(content_json); *command_array_out = command_array; *command_count_out = array_size; return 0; } // Process incoming admin command event (Kind 23456) int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out) { if (!event || !command_array_out || !command_count_out || !admin_pubkey_out) { return -1; } // Get blossom server pubkey from config sqlite3 *db; sqlite3_stmt *stmt; char blossom_pubkey[65] = ""; if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { return -1; } const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char *pubkey = (const char *)sqlite3_column_text(stmt, 0); if (pubkey) { strncpy(blossom_pubkey, pubkey, sizeof(blossom_pubkey) - 1); } } sqlite3_finalize(stmt); } sqlite3_close(db); if (strlen(blossom_pubkey) != 64) { fprintf(stderr, "ERROR: Cannot determine blossom pubkey for admin auth\n"); return -1; } // Check if it's a valid admin command event for us if (!is_admin_command_event(event, blossom_pubkey)) { return -1; } // Validate admin authentication (signature and pubkey) if (!validate_admin_event(event)) { return -1; } // Get admin pubkey from event cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!cJSON_IsString(admin_pubkey_json)) { return -1; } *admin_pubkey_out = malloc(strlen(admin_pubkey_json->valuestring) + 1); if (!*admin_pubkey_out) { fprintf(stderr, "AUTH: Failed to allocate admin pubkey string\n"); return -1; } strcpy(*admin_pubkey_out, admin_pubkey_json->valuestring); if (!*admin_pubkey_out) { return -1; } // Decrypt the command char *decrypted_content = NULL; if (decrypt_admin_command(event, &decrypted_content) != 0) { free(*admin_pubkey_out); *admin_pubkey_out = NULL; return -1; } // Parse the command array if (parse_admin_command(decrypted_content, command_array_out, command_count_out) != 0) { free(decrypted_content); free(*admin_pubkey_out); *admin_pubkey_out = NULL; return -1; } free(decrypted_content); return 0; } // Validate admin pubkey against configured admin int validate_admin_pubkey(const char *pubkey) { if (!pubkey || strlen(pubkey) != 64) { return 0; } sqlite3 *db; sqlite3_stmt *stmt; int result = 0; if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { return 0; } const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char *admin_pubkey = (const char *)sqlite3_column_text(stmt, 0); if (admin_pubkey && strcmp(admin_pubkey, pubkey) == 0) { result = 1; } } sqlite3_finalize(stmt); } sqlite3_close(db); return result; } // Create encrypted response for admin (Kind 23457) int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id __attribute__((unused)), cJSON **response_event_out) { if (!response_json || !admin_pubkey || !response_event_out) { return -1; } // Ensure we have the relay private key if (ensure_keys_loaded() != 0) { return -1; } // Get blossom server pubkey from config sqlite3 *db; sqlite3_stmt *stmt; char blossom_pubkey[65] = ""; if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { return -1; } const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char *pubkey = (const char *)sqlite3_column_text(stmt, 0); if (pubkey) { strncpy(blossom_pubkey, pubkey, sizeof(blossom_pubkey) - 1); } } sqlite3_finalize(stmt); } sqlite3_close(db); if (strlen(blossom_pubkey) != 64) { fprintf(stderr, "ERROR: Cannot determine blossom pubkey for response\n"); return -1; } // Convert hex keys to bytes unsigned char blossom_private_key[32]; unsigned char admin_public_key[32]; if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) { fprintf(stderr, "AUTH: Failed to parse blossom private key\n"); return -1; } if (nostr_hex_to_bytes(admin_pubkey, admin_public_key, 32) != 0) { fprintf(stderr, "AUTH: Failed to parse admin public key\n"); return -1; } // Encrypt response using NIP-44 char encrypted_content[8192]; int result = nostr_nip44_encrypt( blossom_private_key, admin_public_key, response_json, encrypted_content, sizeof(encrypted_content) ); if (result != NOSTR_SUCCESS) { fprintf(stderr, "AUTH: NIP-44 encryption failed with error code %d\n", result); return -1; } // Create Kind 23457 response event cJSON *response_event = cJSON_CreateObject(); if (!response_event) { fprintf(stderr, "AUTH: Failed to create response event JSON\n"); return -1; } // Set event fields cJSON_AddNumberToObject(response_event, "kind", 23457); cJSON_AddStringToObject(response_event, "pubkey", blossom_pubkey); cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL)); cJSON_AddStringToObject(response_event, "content", encrypted_content); // Add tags array with 'p' tag for admin cJSON *tags = cJSON_CreateArray(); cJSON *p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey)); cJSON_AddItemToArray(tags, p_tag); cJSON_AddItemToObject(response_event, "tags", tags); // Sign the event with blossom private key // Convert private key hex to bytes unsigned char blossom_private_key_bytes[32]; if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key_bytes, 32) != 0) { fprintf(stderr, "AUTH: Failed to parse blossom private key for signing\n"); cJSON_Delete(response_event); return -1; } // Create a temporary event structure for signing cJSON* temp_event = cJSON_Duplicate(response_event, 1); if (!temp_event) { fprintf(stderr, "AUTH: Failed to create temp event for signing\n"); cJSON_Delete(response_event); return -1; } // Sign the event using nostr_core_lib cJSON* signed_event = nostr_create_and_sign_event( 23457, // Kind 23457 (admin response) encrypted_content, // content cJSON_GetObjectItem(response_event, "tags"), // tags blossom_private_key_bytes, // private key (time_t)cJSON_GetNumberValue(cJSON_GetObjectItem(response_event, "created_at")) // timestamp ); if (!signed_event) { fprintf(stderr, "AUTH: Failed to sign admin response event\n"); cJSON_Delete(response_event); cJSON_Delete(temp_event); return -1; } // Extract id and signature from signed event cJSON* signed_id = cJSON_GetObjectItem(signed_event, "id"); cJSON* signed_sig = cJSON_GetObjectItem(signed_event, "sig"); if (signed_id && signed_sig) { cJSON_AddStringToObject(response_event, "id", cJSON_GetStringValue(signed_id)); cJSON_AddStringToObject(response_event, "sig", cJSON_GetStringValue(signed_sig)); } else { fprintf(stderr, "AUTH: Signed event missing id or sig\n"); cJSON_Delete(response_event); cJSON_Delete(signed_event); cJSON_Delete(temp_event); return -1; } // Clean up temporary structures cJSON_Delete(signed_event); cJSON_Delete(temp_event); *response_event_out = response_event; return 0; } // Free command array allocated by parse_admin_command void free_command_array(char **command_array, int command_count) { if (command_array) { for (int i = 0; i < command_count; i++) { if (command_array[i]) { free(command_array[i]); } } free(command_array); } }