509 lines
16 KiB
C
509 lines
16 KiB
C
/*
|
|
* 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 <cjson/cJSON.h>
|
|
#include <sqlite3.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
// 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);
|
|
}
|
|
} |