upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-17 05:27:06 +0530
committerYour Name <you@example.com>2026-05-17 05:27:06 +0530
commitfdf662f8f1a1a3b38fe4d251982fffab8e9bf664 (patch)
tree2413bdc936b757adf4849a522b7df2a5c8eb0aec
parentedd125d0e3fe5fe7c0edf30c429723f3b0120c68 (diff)
Phase 7: MCP handler (25 tests), NIP-04 encrypt/decrypt (15 tests), CVM server skeleton
- mcp_handler.c/h: 4 tools (get_config, set_config, get_balance, wallet_send) - nip04.c/h: AES-256-CBC + ECDH with 0x02 compressed pubkey prefix - Fixed IV copy bug: mbedTLS AES-CBC modifies IV in-place - Base64 encode/decode for ciphertext transport - PKCS7 padding - cvm_server.c/h: Nostr DM listener with FreeRTOS task - config: cvm_enabled, cvm_relays fields - 156 total tests passing across 10 test binaries
-rw-r--r--main/CMakeLists.txt3
-rw-r--r--main/config.c12
-rw-r--r--main/config.h3
-rw-r--r--main/cvm_server.c238
-rw-r--r--main/cvm_server.h10
-rw-r--r--main/mcp_handler.c175
-rw-r--r--main/mcp_handler.h36
-rw-r--r--main/nip04.c201
-rw-r--r--main/nip04.h13
-rw-r--r--main/tollgate_main.c8
-rw-r--r--tests/unit/Makefile9
-rw-r--r--tests/unit/stubs/esp_log.h1
-rw-r--r--tests/unit/stubs/esp_system.h10
-rw-r--r--tests/unit/test_mcp_handler.c148
-rw-r--r--tests/unit/test_nip04.c107
15 files changed, 973 insertions, 1 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index c2aaeb2..91748f2 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -13,6 +13,9 @@ idf_component_register(SRCS "tollgate_main.c"
13 "tollgate_client.c" 13 "tollgate_client.c"
14 "lnurl_pay.c" 14 "lnurl_pay.c"
15 "lightning_payout.c" 15 "lightning_payout.c"
16 "nip04.c"
17 "mcp_handler.c"
18 "cvm_server.c"
16 INCLUDE_DIRS "." 19 INCLUDE_DIRS "."
17 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server 20 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server
18 lwip json esp_http_client mbedtls esp-tls log spiffs 21 lwip json esp_http_client mbedtls esp-tls log spiffs
diff --git a/main/config.c b/main/config.c
index 3e01efc..2ee8637 100644
--- a/main/config.c
+++ b/main/config.c
@@ -33,6 +33,8 @@ esp_err_t tollgate_config_init(void)
33 g_config.payout.check_interval_s = 60; 33 g_config.payout.check_interval_s = 60;
34 g_config.payout.recipient_count = 0; 34 g_config.payout.recipient_count = 0;
35 g_config.payout.mint_count = 0; 35 g_config.payout.mint_count = 0;
36 g_config.cvm_enabled = false;
37 strncpy(g_config.cvm_relays, "wss://relay.damus.io", sizeof(g_config.cvm_relays) - 1);
36 38
37 esp_vfs_spiffs_conf_t conf = { 39 esp_vfs_spiffs_conf_t conf = {
38 .base_path = "/spiffs", 40 .base_path = "/spiffs",
@@ -235,6 +237,16 @@ esp_err_t tollgate_config_init(void)
235 } 237 }
236 } 238 }
237 239
240 cJSON *cvm = cJSON_GetObjectItem(root, "cvm");
241 if (cvm && cJSON_IsObject(cvm)) {
242 cJSON *cvm_en = cJSON_GetObjectItem(cvm, "enabled");
243 if (cvm_en && cJSON_IsBool(cvm_en)) g_config.cvm_enabled = cJSON_IsTrue(cvm_en);
244 cJSON *cvm_relays = cJSON_GetObjectItem(cvm, "relays");
245 if (cvm_relays && cJSON_IsString(cvm_relays)) {
246 strncpy(g_config.cvm_relays, cvm_relays->valuestring, sizeof(g_config.cvm_relays) - 1);
247 }
248 }
249
238 if (g_config.payout.mint_count == 0 && g_config.mint_url[0] != '\0') { 250 if (g_config.payout.mint_count == 0 && g_config.mint_url[0] != '\0') {
239 strncpy(g_config.payout.mints[0].url, g_config.mint_url, 251 strncpy(g_config.payout.mints[0].url, g_config.mint_url,
240 sizeof(g_config.payout.mints[0].url) - 1); 252 sizeof(g_config.payout.mints[0].url) - 1);
diff --git a/main/config.h b/main/config.h
index 86b5e1a..fa4d95c 100644
--- a/main/config.h
+++ b/main/config.h
@@ -60,6 +60,9 @@ typedef struct {
60 int client_retry_interval_ms; 60 int client_retry_interval_ms;
61 61
62 payout_config_t payout; 62 payout_config_t payout;
63
64 bool cvm_enabled;
65 char cvm_relays[256];
63} tollgate_config_t; 66} tollgate_config_t;
64 67
65void tollgate_config_derive_unique(tollgate_config_t *cfg); 68void tollgate_config_derive_unique(tollgate_config_t *cfg);
diff --git a/main/cvm_server.c b/main/cvm_server.c
new file mode 100644
index 0000000..5addd88
--- /dev/null
+++ b/main/cvm_server.c
@@ -0,0 +1,238 @@
1#include "cvm_server.h"
2#include "mcp_handler.h"
3#include "nip04.h"
4#include "identity.h"
5#include "config.h"
6#include "nucula_wallet.h"
7#include "cJSON.h"
8#include "esp_log.h"
9#include "esp_http_client.h"
10#include "freertos/FreeRTOS.h"
11#include "freertos/task.h"
12#include <string.h>
13#include <stdio.h>
14
15static const char *TAG = "cvm_server";
16
17static bool g_running = false;
18static TaskHandle_t g_task = NULL;
19
20static const char *DEFAULT_RELAY = "wss://relay.damus.io";
21
22static char *fetch_relays(void)
23{
24 const tollgate_config_t *cfg = tollgate_config_get();
25 if (cfg && cfg->cvm_relays[0]) {
26 return cfg->cvm_relays;
27 }
28 return (char *)DEFAULT_RELAY;
29}
30
31static char *http_get(const char *url, int timeout_ms)
32{
33 char *buf = malloc(8192);
34 if (!buf) return NULL;
35 int total = 0;
36
37 esp_http_client_config_t config = {
38 .url = url,
39 .method = HTTP_METHOD_GET,
40 .timeout_ms = timeout_ms,
41 };
42 esp_http_client_handle_t client = esp_http_client_init(&config);
43 if (!client) { free(buf); return NULL; }
44
45 esp_err_t err = esp_http_client_open(client, 0);
46 if (err != ESP_OK) {
47 esp_http_client_cleanup(client);
48 free(buf);
49 return NULL;
50 }
51
52 int content_length = esp_http_client_fetch_headers(client);
53 int max_read = content_length > 0 ? content_length : 8191;
54
55 while (total < max_read) {
56 int n = esp_http_client_read(client, buf + total, max_read - total);
57 if (n <= 0) break;
58 total += n;
59 }
60 buf[total] = '\0';
61 esp_http_client_cleanup(client);
62 return buf;
63}
64
65static cJSON *build_filter(const char *npub)
66{
67 cJSON *filter = cJSON_CreateObject();
68 cJSON *kinds = cJSON_CreateArray();
69 cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4));
70 cJSON_AddItemToObject(filter, "kinds", kinds);
71 cJSON_AddStringToObject(filter, "#p", npub);
72 cJSON_AddNumberToObject(filter, "limit", 10);
73 return filter;
74}
75
76static cJSON *build_subscription(const char *npub)
77{
78 cJSON *sub = cJSON_CreateArray();
79 cJSON_AddItemToArray(sub, cJSON_CreateString("REQ"));
80 cJSON_AddItemToArray(sub, cJSON_CreateString("cvm_sub_01"));
81 cJSON_AddItemToArray(sub, build_filter(npub));
82 return sub;
83}
84
85static void process_dm(const char *sender_pubkey, const char *encrypted_content)
86{
87 const tollgate_identity_t *id = identity_get();
88 if (!id || !id->initialized) {
89 ESP_LOGE(TAG, "Identity not initialized");
90 return;
91 }
92
93 uint8_t sender_pk[64];
94 for (int i = 0; i < 64; i++) {
95 char hex[3] = { sender_pubkey[i*2], sender_pubkey[i*2+1], 0 };
96 sender_pk[i] = (uint8_t)strtol(hex, NULL, 16);
97 }
98
99 char plaintext[2048];
100 int pt_len = nip04_decrypt(id->nsec, sender_pk, encrypted_content, plaintext, sizeof(plaintext));
101 if (pt_len < 0) {
102 ESP_LOGE(TAG, "Failed to decrypt DM from %.8s", sender_pubkey);
103 return;
104 }
105
106 ESP_LOGI(TAG, "Decrypted DM from %.8s: %s", sender_pubkey, plaintext);
107
108 cJSON *msg = cJSON_Parse(plaintext);
109 if (!msg) {
110 ESP_LOGE(TAG, "Invalid JSON in DM");
111 return;
112 }
113
114 cJSON *method = cJSON_GetObjectItem(msg, "method");
115 cJSON *params = cJSON_GetObjectItem(msg, "params");
116 if (!method || !cJSON_IsString(method)) {
117 cJSON_Delete(msg);
118 ESP_LOGE(TAG, "Missing 'method' in CVM request");
119 return;
120 }
121
122 mcp_request_t req = {0};
123 req.tool = mcp_parse_tool(method->valuestring);
124 strncpy(req.method, method->valuestring, sizeof(req.method) - 1);
125 if (params && cJSON_IsString(params)) {
126 strncpy(req.params_json, params->valuestring, sizeof(req.params_json) - 1);
127 } else if (params) {
128 char *pjson = cJSON_PrintUnformatted(params);
129 strncpy(req.params_json, pjson, sizeof(req.params_json) - 1);
130 cJSON_free(pjson);
131 }
132
133 mcp_response_t resp = mcp_dispatch(&req);
134 cJSON_Delete(msg);
135
136 cJSON *response_msg = cJSON_CreateObject();
137 if (resp.success) {
138 cJSON_AddStringToObject(response_msg, "status", "ok");
139 cJSON_AddItemToObject(response_msg, "result", cJSON_Parse(resp.result_json));
140 } else {
141 cJSON_AddStringToObject(response_msg, "status", "error");
142 cJSON_AddStringToObject(response_msg, "error", resp.error);
143 }
144
145 char *response_str = cJSON_PrintUnformatted(response_msg);
146 cJSON_Delete(response_msg);
147
148 uint8_t response_ct[4096];
149 size_t ct_len = 0;
150 nip04_encrypt(id->nsec, sender_pk, response_str, response_ct, &ct_len);
151 free(response_str);
152
153 ESP_LOGI(TAG, "CVM response prepared (%zu bytes encrypted), would send to %.8s", ct_len, sender_pubkey);
154}
155
156static void parse_nostr_events(const char *data)
157{
158 cJSON *arr = cJSON_Parse(data);
159 if (!arr || !cJSON_IsArray(arr)) {
160 if (arr) cJSON_Delete(arr);
161 return;
162 }
163
164 cJSON *item = NULL;
165 cJSON_ArrayForEach(item, arr) {
166 if (!cJSON_IsArray(item)) continue;
167 int arr_size = cJSON_GetArraySize(item);
168 if (arr_size < 3) continue;
169
170 cJSON *cmd = cJSON_GetArrayItem(item, 0);
171 if (!cmd || !cJSON_IsString(cmd) || strcmp(cmd->valuestring, "EVENT") != 0) continue;
172
173 cJSON *event = cJSON_GetArrayItem(item, 2);
174 if (!event) continue;
175
176 cJSON *kind = cJSON_GetObjectItem(event, "kind");
177 if (!kind || kind->valueint != 4) continue;
178
179 cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey");
180 cJSON *content = cJSON_GetObjectItem(event, "content");
181 if (pubkey && content) {
182 process_dm(pubkey->valuestring, content->valuestring);
183 }
184 }
185 cJSON_Delete(arr);
186}
187
188static void cvm_task(void *arg)
189{
190 const tollgate_identity_t *id = identity_get();
191 if (!id || !id->initialized) {
192 ESP_LOGE(TAG, "Cannot start: identity not initialized");
193 vTaskDelete(NULL);
194 return;
195 }
196
197 char *relays = fetch_relays();
198 ESP_LOGI(TAG, "CVM server started, relays: %s", relays);
199
200 while (g_running) {
201 ESP_LOGI(TAG, "Polling for DMs...");
202
203 cJSON *sub = build_subscription(id->npub_hex);
204 char *sub_json = cJSON_PrintUnformatted(sub);
205 cJSON_Delete(sub);
206
207 char url[256];
208 snprintf(url, sizeof(url), "%s/cvm_poll", relays);
209 free(sub_json);
210
211 vTaskDelay(pdMS_TO_TICKS(30000));
212 }
213
214 ESP_LOGI(TAG, "CVM server stopped");
215 vTaskDelete(NULL);
216}
217
218esp_err_t cvm_server_init(void)
219{
220 ESP_LOGI(TAG, "CVM server initialized");
221 return ESP_OK;
222}
223
224void cvm_server_start(void)
225{
226 if (g_running) return;
227 g_running = true;
228 xTaskCreate(cvm_task, "cvm_server", 8192, NULL, 5, &g_task);
229}
230
231void cvm_server_stop(void)
232{
233 g_running = false;
234 if (g_task) {
235 vTaskDelay(pdMS_TO_TICKS(500));
236 g_task = NULL;
237 }
238}
diff --git a/main/cvm_server.h b/main/cvm_server.h
new file mode 100644
index 0000000..d336514
--- /dev/null
+++ b/main/cvm_server.h
@@ -0,0 +1,10 @@
1#ifndef CVM_SERVER_H
2#define CVM_SERVER_H
3
4#include "esp_err.h"
5
6esp_err_t cvm_server_init(void);
7void cvm_server_start(void);
8void cvm_server_stop(void);
9
10#endif
diff --git a/main/mcp_handler.c b/main/mcp_handler.c
new file mode 100644
index 0000000..f40c1bd
--- /dev/null
+++ b/main/mcp_handler.c
@@ -0,0 +1,175 @@
1#include "mcp_handler.h"
2#include "config.h"
3#include "nucula_wallet.h"
4#include "cJSON.h"
5#include <string.h>
6#include <stdio.h>
7
8static const char *TAG = "mcp_handler";
9
10mcp_tool_t mcp_parse_tool(const char *method)
11{
12 if (!method) return MCP_TOOL_UNKNOWN;
13 if (strcmp(method, "get_config") == 0) return MCP_TOOL_GET_CONFIG;
14 if (strcmp(method, "set_config") == 0) return MCP_TOOL_SET_CONFIG;
15 if (strcmp(method, "get_balance") == 0) return MCP_TOOL_GET_BALANCE;
16 if (strcmp(method, "wallet_send") == 0) return MCP_TOOL_WALLET_SEND;
17 return MCP_TOOL_UNKNOWN;
18}
19
20mcp_response_t mcp_handle_get_config(void)
21{
22 mcp_response_t resp = {0};
23 const tollgate_config_t *cfg = tollgate_config_get();
24 if (!cfg) {
25 resp.success = false;
26 snprintf(resp.error, sizeof(resp.error), "Config not loaded");
27 return resp;
28 }
29
30 cJSON *root = cJSON_CreateObject();
31 cJSON_AddStringToObject(root, "ssid", cfg->ap_ssid);
32 cJSON_AddStringToObject(root, "metric", cfg->metric);
33 cJSON_AddNumberToObject(root, "price_per_step", cfg->price_per_step);
34 cJSON_AddNumberToObject(root, "step_size_ms", cfg->step_size_ms);
35 cJSON_AddNumberToObject(root, "step_size_bytes", cfg->step_size_bytes);
36 cJSON_AddStringToObject(root, "mint_url", cfg->mint_url);
37 cJSON_AddBoolToObject(root, "client_enabled", cfg->client_enabled);
38 cJSON_AddBoolToObject(root, "payout_enabled", cfg->payout.enabled);
39 cJSON_AddStringToObject(root, "wifi_ssid",
40 cfg->network_count > 0 ? cfg->networks[0].ssid : "");
41
42 char *json = cJSON_PrintUnformatted(root);
43 snprintf(resp.result_json, sizeof(resp.result_json), "%s", json);
44 cJSON_free(json);
45 cJSON_Delete(root);
46 resp.success = true;
47 return resp;
48}
49
50mcp_response_t mcp_handle_set_config(const char *params_json)
51{
52 mcp_response_t resp = {0};
53 cJSON *root = cJSON_Parse(params_json);
54 if (!root) {
55 resp.success = false;
56 snprintf(resp.error, sizeof(resp.error), "Invalid JSON params");
57 return resp;
58 }
59
60 tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get();
61 if (!cfg) {
62 cJSON_Delete(root);
63 resp.success = false;
64 snprintf(resp.error, sizeof(resp.error), "Config not loaded");
65 return resp;
66 }
67
68 cJSON *item;
69 item = cJSON_GetObjectItem(root, "price_per_step");
70 if (item && cJSON_IsNumber(item)) cfg->price_per_step = item->valueint;
71 item = cJSON_GetObjectItem(root, "step_size_ms");
72 if (item && cJSON_IsNumber(item)) cfg->step_size_ms = item->valueint;
73 item = cJSON_GetObjectItem(root, "step_size_bytes");
74 if (item && cJSON_IsNumber(item)) cfg->step_size_bytes = item->valueint;
75 item = cJSON_GetObjectItem(root, "client_enabled");
76 if (item && cJSON_IsBool(item)) cfg->client_enabled = cJSON_IsTrue(item);
77 item = cJSON_GetObjectItem(root, "payout_enabled");
78 if (item && cJSON_IsBool(item)) cfg->payout.enabled = cJSON_IsTrue(item);
79 item = cJSON_GetObjectItem(root, "metric");
80 if (item && cJSON_IsString(item)) {
81 strncpy(cfg->metric, item->valuestring, sizeof(cfg->metric) - 1);
82 }
83
84 cJSON_Delete(root);
85 resp.success = true;
86 snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\"}");
87 return resp;
88}
89
90mcp_response_t mcp_handle_get_balance(void)
91{
92 mcp_response_t resp = {0};
93 uint64_t balance = nucula_wallet_balance();
94 int proof_count = nucula_wallet_proof_count();
95
96 cJSON *root = cJSON_CreateObject();
97 cJSON_AddNumberToObject(root, "balance_sats", (double)balance);
98 cJSON_AddNumberToObject(root, "proof_count", proof_count);
99
100 char *json = cJSON_PrintUnformatted(root);
101 snprintf(resp.result_json, sizeof(resp.result_json), "%s", json);
102 cJSON_free(json);
103 cJSON_Delete(root);
104 resp.success = true;
105 return resp;
106}
107
108mcp_response_t mcp_handle_wallet_send(const char *params_json)
109{
110 mcp_response_t resp = {0};
111 cJSON *root = cJSON_Parse(params_json);
112 if (!root) {
113 resp.success = false;
114 snprintf(resp.error, sizeof(resp.error), "Invalid JSON params");
115 return resp;
116 }
117
118 cJSON *amount_item = cJSON_GetObjectItem(root, "amount");
119 if (!amount_item || !cJSON_IsNumber(amount_item)) {
120 cJSON_Delete(root);
121 resp.success = false;
122 snprintf(resp.error, sizeof(resp.error), "Missing 'amount' field");
123 return resp;
124 }
125
126 uint64_t amount = (uint64_t)amount_item->valuedouble;
127 char token_out[2048] = {0};
128 int rc = nucula_wallet_send(amount, token_out, sizeof(token_out));
129
130 if (rc != 0) {
131 cJSON_Delete(root);
132 resp.success = false;
133 snprintf(resp.error, sizeof(resp.error), "Send failed: %d", rc);
134 return resp;
135 }
136
137 cJSON *result = cJSON_CreateObject();
138 cJSON_AddStringToObject(result, "token", token_out);
139 cJSON_AddNumberToObject(result, "amount", (double)amount);
140 char *json = cJSON_PrintUnformatted(result);
141 snprintf(resp.result_json, sizeof(resp.result_json), "%s", json);
142 cJSON_free(json);
143 cJSON_Delete(result);
144 cJSON_Delete(root);
145 resp.success = true;
146 return resp;
147}
148
149mcp_response_t mcp_dispatch(const mcp_request_t *req)
150{
151 if (!req) {
152 mcp_response_t resp = {0};
153 resp.success = false;
154 snprintf(resp.error, sizeof(resp.error), "NULL request");
155 return resp;
156 }
157
158 switch (req->tool) {
159 case MCP_TOOL_GET_CONFIG:
160 return mcp_handle_get_config();
161 case MCP_TOOL_SET_CONFIG:
162 return mcp_handle_set_config(req->params_json);
163 case MCP_TOOL_GET_BALANCE:
164 return mcp_handle_get_balance();
165 case MCP_TOOL_WALLET_SEND:
166 return mcp_handle_wallet_send(req->params_json);
167 default:
168 break;
169 }
170
171 mcp_response_t resp = {0};
172 resp.success = false;
173 snprintf(resp.error, sizeof(resp.error), "Unknown tool: %s", req->method);
174 return resp;
175}
diff --git a/main/mcp_handler.h b/main/mcp_handler.h
new file mode 100644
index 0000000..e42b5ee
--- /dev/null
+++ b/main/mcp_handler.h
@@ -0,0 +1,36 @@
1#ifndef MCP_HANDLER_H
2#define MCP_HANDLER_H
3
4#include <stdint.h>
5#include <stdbool.h>
6
7typedef enum {
8 MCP_TOOL_GET_CONFIG = 0,
9 MCP_TOOL_SET_CONFIG = 1,
10 MCP_TOOL_GET_BALANCE = 2,
11 MCP_TOOL_WALLET_SEND = 3,
12 MCP_TOOL_UNKNOWN = 99
13} mcp_tool_t;
14
15typedef struct {
16 mcp_tool_t tool;
17 char method[64];
18 char params_json[1024];
19} mcp_request_t;
20
21typedef struct {
22 bool success;
23 char result_json[2048];
24 char error[256];
25} mcp_response_t;
26
27mcp_tool_t mcp_parse_tool(const char *method);
28
29mcp_response_t mcp_handle_get_config(void);
30mcp_response_t mcp_handle_set_config(const char *params_json);
31mcp_response_t mcp_handle_get_balance(void);
32mcp_response_t mcp_handle_wallet_send(const char *params_json);
33
34mcp_response_t mcp_dispatch(const mcp_request_t *req);
35
36#endif
diff --git a/main/nip04.c b/main/nip04.c
new file mode 100644
index 0000000..5526d4f
--- /dev/null
+++ b/main/nip04.c
@@ -0,0 +1,201 @@
1#include "nip04.h"
2#include "esp_log.h"
3#include "esp_system.h"
4#include "mbedtls/aes.h"
5#include <string.h>
6#include <stdlib.h>
7
8#include <secp256k1.h>
9#include <secp256k1_ecdh.h>
10
11static const char *TAG = "nip04";
12
13static const unsigned char base64_table[] =
14 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15
16static size_t base64_encode(const uint8_t *src, size_t len, char *dst)
17{
18 size_t j = 0;
19 for (size_t i = 0; i < len; i += 3) {
20 uint32_t a = src[i];
21 uint32_t b = (i + 1 < len) ? src[i + 1] : 0;
22 uint32_t c = (i + 2 < len) ? src[i + 2] : 0;
23 uint32_t triple = (a << 16) | (b << 8) | c;
24 dst[j++] = base64_table[(triple >> 18) & 0x3F];
25 dst[j++] = base64_table[(triple >> 12) & 0x3F];
26 dst[j++] = (i + 1 < len) ? base64_table[(triple >> 6) & 0x3F] : '=';
27 dst[j++] = (i + 2 < len) ? base64_table[triple & 0x3F] : '=';
28 }
29 dst[j] = '\0';
30 return j;
31}
32
33static int base64_val(char c)
34{
35 if (c >= 'A' && c <= 'Z') return c - 'A';
36 if (c >= 'a' && c <= 'z') return c - 'a' + 26;
37 if (c >= '0' && c <= '9') return c - '0' + 52;
38 if (c == '+') return 62;
39 if (c == '/') return 63;
40 return -1;
41}
42
43static size_t base64_decode(const char *src, size_t len, uint8_t *dst)
44{
45 size_t padding = 0;
46 if (len >= 1 && src[len - 1] == '=') padding++;
47 if (len >= 2 && src[len - 2] == '=') padding++;
48 size_t expected = (len / 4) * 3 - padding;
49
50 size_t j = 0;
51 for (size_t i = 0; i + 3 < len; i += 4) {
52 uint32_t a = (uint32_t)base64_val(src[i]);
53 uint32_t b = (uint32_t)base64_val(src[i + 1]);
54 uint32_t c = (src[i + 2] != '=') ? (uint32_t)base64_val(src[i + 2]) : 0;
55 uint32_t d = (src[i + 3] != '=') ? (uint32_t)base64_val(src[i + 3]) : 0;
56 uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d;
57 if (j < expected) dst[j++] = (triple >> 16) & 0xFF;
58 if (j < expected) dst[j++] = (triple >> 8) & 0xFF;
59 if (j < expected) dst[j++] = triple & 0xFF;
60 }
61 return j;
62}
63
64static int ecdh_xonly_hash(unsigned char *output, const uint8_t *x32, const uint8_t *y32, void *data)
65{
66 (void)y32;
67 (void)data;
68 memcpy(output, x32, 32);
69 return 1;
70}
71
72static void compute_shared_secret(const uint8_t *privkey, const uint8_t *pubkey32,
73 uint8_t shared_secret[32])
74{
75 if (!privkey || !pubkey32) {
76 memset(shared_secret, 0, 32);
77 return;
78 }
79
80 uint8_t compressed[33];
81 compressed[0] = 0x02;
82 memcpy(compressed + 1, pubkey32, 32);
83
84 secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
85 secp256k1_pubkey pk;
86 if (!secp256k1_ec_pubkey_parse(ctx, &pk, compressed, 33)) {
87 ESP_LOGE(TAG, "Failed to parse compressed pubkey");
88 memset(shared_secret, 0, 32);
89 secp256k1_context_destroy(ctx);
90 return;
91 }
92
93 uint8_t shared[32];
94 secp256k1_ecdh(ctx, shared, &pk, privkey, ecdh_xonly_hash, NULL);
95 memcpy(shared_secret, shared, 32);
96
97 ESP_LOGI(TAG, "Shared secret: %02x%02x%02x%02x... (pubkey32=%02x%02x%02x%02x...)",
98 shared[0], shared[1], shared[2], shared[3],
99 pubkey32[0], pubkey32[1], pubkey32[2], pubkey32[3]);
100
101 secp256k1_context_destroy(ctx);
102}
103
104static void pkcs7_pad(uint8_t *buf, size_t data_len, size_t block_size, size_t *padded_len)
105{
106 size_t pad = block_size - (data_len % block_size);
107 for (size_t i = 0; i < pad; i++) {
108 buf[data_len + i] = (uint8_t)pad;
109 }
110 *padded_len = data_len + pad;
111}
112
113static size_t pkcs7_unpad(const uint8_t *buf, size_t len)
114{
115 if (len == 0) return 0;
116 uint8_t pad = buf[len - 1];
117 if (pad == 0 || pad > 16) return 0;
118 for (size_t i = len - pad; i < len; i++) {
119 if (buf[i] != pad) return 0;
120 }
121 return len - pad;
122}
123
124void nip04_encrypt(const uint8_t *sender_privkey, const uint8_t *recipient_pubkey,
125 const char *plaintext, uint8_t *ciphertext_base64, size_t *out_len)
126{
127 uint8_t shared_secret[32];
128 compute_shared_secret(sender_privkey, recipient_pubkey, shared_secret);
129
130 size_t pt_len = strlen(plaintext);
131 uint8_t iv[16];
132 esp_fill_random(iv, 16);
133 uint8_t iv_copy[16];
134 memcpy(iv_copy, iv, 16);
135
136 uint8_t padded[4096];
137 memcpy(padded, plaintext, pt_len);
138 size_t padded_len;
139 pkcs7_pad(padded, pt_len, 16, &padded_len);
140
141 mbedtls_aes_context aes;
142 mbedtls_aes_init(&aes);
143 mbedtls_aes_setkey_enc(&aes, shared_secret, 256);
144 mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, padded_len, iv, padded, padded);
145 mbedtls_aes_free(&aes);
146
147 size_t total = 16 + padded_len;
148 uint8_t combined[4112];
149 memcpy(combined, iv_copy, 16);
150 memcpy(combined + 16, padded, padded_len);
151
152 *out_len = base64_encode(combined, total, (char *)ciphertext_base64);
153 ((char *)ciphertext_base64)[*out_len] = '\0';
154
155 ESP_LOGD(TAG, "Encrypted %zu bytes -> %zu base64", pt_len, *out_len);
156}
157
158int nip04_decrypt(const uint8_t *recipient_privkey, const uint8_t *sender_pubkey,
159 const char *ciphertext_base64, char *plaintext, size_t plaintext_max)
160{
161 uint8_t shared_secret[32];
162 compute_shared_secret(recipient_privkey, sender_pubkey, shared_secret);
163
164 size_t b64_len = strlen(ciphertext_base64);
165 uint8_t combined[4112];
166 size_t combined_len = base64_decode(ciphertext_base64, b64_len, combined);
167
168 if (combined_len < 32) {
169 ESP_LOGE(TAG, "Ciphertext too short: %zu", combined_len);
170 return -1;
171 }
172
173 uint8_t iv[16];
174 memcpy(iv, combined, 16);
175
176 size_t ct_len = combined_len - 16;
177 uint8_t ct[4096];
178 memcpy(ct, combined + 16, ct_len);
179
180 mbedtls_aes_context aes;
181 mbedtls_aes_init(&aes);
182 mbedtls_aes_setkey_dec(&aes, shared_secret, 256);
183 mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, ct_len, iv, ct, ct);
184 mbedtls_aes_free(&aes);
185
186 size_t pt_len = pkcs7_unpad(ct, ct_len);
187 if (pt_len == 0 || pt_len >= plaintext_max) {
188 ESP_LOGE(TAG, "Invalid padding: pt_len=%zu ct_len=%zu last_byte=%d padded_bytes:",
189 pt_len, ct_len, ct_len > 0 ? ct[ct_len-1] : -1);
190 for (size_t i = ct_len > 4 ? ct_len - 4 : 0; i < ct_len; i++) {
191 ESP_LOGE(TAG, " ct[%zu]=%02x", i, ct[i]);
192 }
193 return -1;
194 }
195
196 memcpy(plaintext, ct, pt_len);
197 plaintext[pt_len] = '\0';
198
199 ESP_LOGD(TAG, "Decrypted %zu base64 -> %zu bytes", b64_len, pt_len);
200 return (int)pt_len;
201}
diff --git a/main/nip04.h b/main/nip04.h
new file mode 100644
index 0000000..f612a8e
--- /dev/null
+++ b/main/nip04.h
@@ -0,0 +1,13 @@
1#ifndef NIP04_H
2#define NIP04_H
3
4#include <stdint.h>
5#include <stddef.h>
6
7void nip04_encrypt(const uint8_t *sender_privkey, const uint8_t *recipient_pubkey,
8 const char *plaintext, uint8_t *ciphertext_base64, size_t *out_len);
9
10int nip04_decrypt(const uint8_t *recipient_privkey, const uint8_t *sender_pubkey,
11 const char *ciphertext_base64, char *plaintext, size_t plaintext_max);
12
13#endif
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index 3f83923..6adb0ec 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -21,6 +21,7 @@
21#include "wifistr.h" 21#include "wifistr.h"
22#include "tollgate_client.h" 22#include "tollgate_client.h"
23#include "lightning_payout.h" 23#include "lightning_payout.h"
24#include "cvm_server.h"
24 25
25#define MAX_STA_RETRY 5 26#define MAX_STA_RETRY 5
26static const char *TAG = "tollgate_main"; 27static const char *TAG = "tollgate_main";
@@ -146,6 +147,12 @@ static void start_services(void)
146 147
147 xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); 148 xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL);
148 149
150 const tollgate_config_t *cfg2 = tollgate_config_get();
151 if (cfg2->cvm_enabled) {
152 cvm_server_init();
153 cvm_server_start();
154 }
155
149 s_services_running = true; 156 s_services_running = true;
150 if (s_services_mutex) xSemaphoreGive(s_services_mutex); 157 if (s_services_mutex) xSemaphoreGive(s_services_mutex);
151 ESP_LOGI(TAG, "=== TollGate services started ==="); 158 ESP_LOGI(TAG, "=== TollGate services started ===");
@@ -162,6 +169,7 @@ static void stop_services(void)
162 captive_portal_stop(); 169 captive_portal_stop();
163 tollgate_api_stop(); 170 tollgate_api_stop();
164 dns_server_stop(); 171 dns_server_stop();
172 cvm_server_stop();
165 firewall_disable_nat(); 173 firewall_disable_nat();
166 firewall_revoke_all(); 174 firewall_revoke_all();
167 s_services_running = false; 175 s_services_running = false;
diff --git a/tests/unit/Makefile b/tests/unit/Makefile
index 53bcc2c..5dee0d7 100644
--- a/tests/unit/Makefile
+++ b/tests/unit/Makefile
@@ -10,6 +10,7 @@ CFLAGS := -Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wno-sign-com
10 -std=gnu17 -g -O0 \ 10 -std=gnu17 -g -O0 \
11 -DTEST_HOST \ 11 -DTEST_HOST \
12 -DENABLE_MODULE_SCHNORRSIG=1 -DENABLE_MODULE_EXTRAKEYS=1 \ 12 -DENABLE_MODULE_SCHNORRSIG=1 -DENABLE_MODULE_EXTRAKEYS=1 \
13 -DENABLE_MODULE_ECDH=1 \
13 -DECMULT_WINDOW_SIZE=8 -DECMULT_GEN_PREC_BITS=4 \ 14 -DECMULT_WINDOW_SIZE=8 -DECMULT_GEN_PREC_BITS=4 \
14 -include stubs/esp_err.h \ 15 -include stubs/esp_err.h \
15 -I stubs \ 16 -I stubs \
@@ -21,7 +22,7 @@ LDFLAGS := -lmbedcrypto -lcjson -lm
21 22
22SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o 23SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o
23 24
24TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout 25TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04
25 26
26.PHONY: all test clean $(TESTS) 27.PHONY: all test clean $(TESTS)
27 28
@@ -71,5 +72,11 @@ test_lnurl_pay: test_lnurl_pay.c
71test_lightning_payout: test_lightning_payout.c 72test_lightning_payout: test_lightning_payout.c
72 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 73 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
73 74
75test_mcp_handler: test_mcp_handler.c $(REPO_ROOT)/main/mcp_handler.c
76 $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/mcp_handler.c -o $@ $(LDFLAGS)
77
78test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ)
79 $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS)
80
74clean: 81clean:
75 rm -f $(TESTS) $(SECP256K1_OBJ) 82 rm -f $(TESTS) $(SECP256K1_OBJ)
diff --git a/tests/unit/stubs/esp_log.h b/tests/unit/stubs/esp_log.h
index f353fe9..b9d44b3 100644
--- a/tests/unit/stubs/esp_log.h
+++ b/tests/unit/stubs/esp_log.h
@@ -6,5 +6,6 @@
6#define ESP_LOGI(tag, fmt, ...) do { printf("I %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) 6#define ESP_LOGI(tag, fmt, ...) do { printf("I %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0)
7#define ESP_LOGW(tag, fmt, ...) do { printf("W %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) 7#define ESP_LOGW(tag, fmt, ...) do { printf("W %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0)
8#define ESP_LOGE(tag, fmt, ...) do { fprintf(stderr, "E %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) 8#define ESP_LOGE(tag, fmt, ...) do { fprintf(stderr, "E %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0)
9#define ESP_LOGD(tag, fmt, ...) do { } while(0)
9 10
10#endif 11#endif
diff --git a/tests/unit/stubs/esp_system.h b/tests/unit/stubs/esp_system.h
index 8e63c80..cd54743 100644
--- a/tests/unit/stubs/esp_system.h
+++ b/tests/unit/stubs/esp_system.h
@@ -1,4 +1,14 @@
1#ifndef STUBS_ESP_SYSTEM_H 1#ifndef STUBS_ESP_SYSTEM_H
2#define STUBS_ESP_SYSTEM_H 2#define STUBS_ESP_SYSTEM_H
3 3
4#include <stdlib.h>
5#include <stdint.h>
6
7static inline void esp_fill_random(uint8_t *buf, size_t len)
8{
9 for (size_t i = 0; i < len; i++) {
10 buf[i] = (uint8_t)(rand() & 0xFF);
11 }
12}
13
4#endif 14#endif
diff --git a/tests/unit/test_mcp_handler.c b/tests/unit/test_mcp_handler.c
new file mode 100644
index 0000000..aaa199d
--- /dev/null
+++ b/tests/unit/test_mcp_handler.c
@@ -0,0 +1,148 @@
1#include "test_framework.h"
2#include "mcp_handler.h"
3#include "config.h"
4#include "nucula_wallet.h"
5#include "cJSON.h"
6#include <string.h>
7#include <stdio.h>
8
9static tollgate_config_t g_test_config;
10static uint64_t g_wallet_balance = 0;
11static int g_wallet_proof_count = 0;
12static int g_wallet_send_rc = 0;
13static char g_wallet_send_token[256] = "cashuA_test_token";
14
15const tollgate_config_t *tollgate_config_get(void) {
16 return &g_test_config;
17}
18
19uint64_t nucula_wallet_balance(void) {
20 return g_wallet_balance;
21}
22
23int nucula_wallet_proof_count(void) {
24 return g_wallet_proof_count;
25}
26
27int nucula_wallet_send(uint64_t amount, char *token_out, size_t token_max) {
28 (void)amount;
29 (void)token_max;
30 if (g_wallet_send_rc == 0) {
31 strncpy(token_out, g_wallet_send_token, token_max - 1);
32 }
33 return g_wallet_send_rc;
34}
35
36static void test_mcp_parse_tool(void)
37{
38 printf("\n=== MCP tool parsing ===\n");
39 ASSERT_EQ_INT(MCP_TOOL_GET_CONFIG, mcp_parse_tool("get_config"), "get_config");
40 ASSERT_EQ_INT(MCP_TOOL_SET_CONFIG, mcp_parse_tool("set_config"), "set_config");
41 ASSERT_EQ_INT(MCP_TOOL_GET_BALANCE, mcp_parse_tool("get_balance"), "get_balance");
42 ASSERT_EQ_INT(MCP_TOOL_WALLET_SEND, mcp_parse_tool("wallet_send"), "wallet_send");
43 ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool("foo"), "unknown tool");
44 ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool(NULL), "NULL tool");
45}
46
47static void test_mcp_get_config(void)
48{
49 printf("\n=== MCP get_config ===\n");
50 memset(&g_test_config, 0, sizeof(g_test_config));
51 strncpy(g_test_config.ap_ssid, "TollGate-TEST", sizeof(g_test_config.ap_ssid) - 1);
52 strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1);
53 g_test_config.price_per_step = 21;
54 g_test_config.step_size_ms = 60000;
55 g_test_config.step_size_bytes = 22020096;
56 strncpy(g_test_config.mint_url, "https://testnut.cashu.space", sizeof(g_test_config.mint_url) - 1);
57
58 mcp_response_t resp = mcp_handle_get_config();
59 ASSERT(resp.success, "get_config succeeds");
60
61 cJSON *result = cJSON_Parse(resp.result_json);
62 ASSERT(result != NULL, "result is valid JSON");
63 ASSERT_EQ_STR("bytes", cJSON_GetObjectItem(result, "metric")->valuestring, "metric=bytes");
64 ASSERT_EQ_INT(21, cJSON_GetObjectItem(result, "price_per_step")->valueint, "price=21");
65 cJSON_Delete(result);
66}
67
68static void test_mcp_set_config(void)
69{
70 printf("\n=== MCP set_config ===\n");
71 memset(&g_test_config, 0, sizeof(g_test_config));
72 g_test_config.price_per_step = 21;
73
74 const char *params = "{\"price_per_step\":42,\"metric\":\"milliseconds\"}";
75 mcp_response_t resp = mcp_handle_set_config(params);
76 ASSERT(resp.success, "set_config succeeds");
77 ASSERT_EQ_INT(42, g_test_config.price_per_step, "price updated to 42");
78 ASSERT_EQ_STR("milliseconds", g_test_config.metric, "metric updated");
79
80 resp = mcp_handle_set_config("not json");
81 ASSERT(!resp.success, "invalid JSON fails");
82}
83
84static void test_mcp_get_balance(void)
85{
86 printf("\n=== MCP get_balance ===\n");
87 g_wallet_balance = 500;
88 g_wallet_proof_count = 8;
89
90 mcp_response_t resp = mcp_handle_get_balance();
91 ASSERT(resp.success, "get_balance succeeds");
92
93 cJSON *result = cJSON_Parse(resp.result_json);
94 ASSERT(result != NULL, "result is valid JSON");
95 ASSERT_EQ_INT(500, (int)cJSON_GetObjectItem(result, "balance_sats")->valuedouble, "balance=500");
96 ASSERT_EQ_INT(8, cJSON_GetObjectItem(result, "proof_count")->valueint, "proofs=8");
97 cJSON_Delete(result);
98}
99
100static void test_mcp_wallet_send(void)
101{
102 printf("\n=== MCP wallet_send ===\n");
103 g_wallet_send_rc = 0;
104 strncpy(g_wallet_send_token, "cashuA_send_test", sizeof(g_wallet_send_token) - 1);
105
106 const char *params = "{\"amount\":21}";
107 mcp_response_t resp = mcp_handle_wallet_send(params);
108 ASSERT(resp.success, "wallet_send succeeds");
109
110 cJSON *result = cJSON_Parse(resp.result_json);
111 ASSERT(result != NULL, "result is valid JSON");
112 ASSERT_EQ_STR("cashuA_send_test", cJSON_GetObjectItem(result, "token")->valuestring, "token matches");
113 cJSON_Delete(result);
114
115 printf("\n--- wallet_send missing amount ---\n");
116 resp = mcp_handle_wallet_send("{}");
117 ASSERT(!resp.success, "missing amount fails");
118
119 printf("\n--- wallet_send send fails ---\n");
120 g_wallet_send_rc = -1;
121 resp = mcp_handle_wallet_send("{\"amount\":100}");
122 ASSERT(!resp.success, "send failure reported");
123}
124
125static void test_mcp_dispatch(void)
126{
127 printf("\n=== MCP dispatch ===\n");
128 mcp_request_t req = {0};
129 req.tool = MCP_TOOL_UNKNOWN;
130 strncpy(req.method, "bogus", sizeof(req.method) - 1);
131 mcp_response_t resp = mcp_dispatch(&req);
132 ASSERT(!resp.success, "unknown tool dispatch fails");
133
134 resp = mcp_dispatch(NULL);
135 ASSERT(!resp.success, "NULL request dispatch fails");
136}
137
138int main(void)
139{
140 printf("=== test_mcp_handler ===\n");
141 test_mcp_parse_tool();
142 test_mcp_get_config();
143 test_mcp_set_config();
144 test_mcp_get_balance();
145 test_mcp_wallet_send();
146 test_mcp_dispatch();
147 TEST_SUMMARY();
148}
diff --git a/tests/unit/test_nip04.c b/tests/unit/test_nip04.c
new file mode 100644
index 0000000..27eb13c
--- /dev/null
+++ b/tests/unit/test_nip04.c
@@ -0,0 +1,107 @@
1#include "test_framework.h"
2#include "../../main/nip04.h"
3#include <secp256k1.h>
4#include <secp256k1_ecdh.h>
5#include <secp256k1_extrakeys.h>
6#include <string.h>
7#include <stdio.h>
8#include <stdlib.h>
9#include <stdbool.h>
10
11static void test_nip04_roundtrip(void)
12{
13 printf("\n=== NIP-04 encrypt/decrypt roundtrip ===\n");
14
15 secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
16
17 uint8_t alice_sec[32], bob_sec[32];
18 memset(alice_sec, 0x01, 32);
19 memset(bob_sec, 0x02, 32);
20
21 secp256k1_pubkey alice_pk, bob_pk;
22 secp256k1_ec_pubkey_create(ctx, &alice_pk, alice_sec);
23 secp256k1_ec_pubkey_create(ctx, &bob_pk, bob_sec);
24
25 uint8_t alice_xonly[32], bob_xonly[32];
26 secp256k1_xonly_pubkey alice_xpk, bob_xpk;
27 secp256k1_xonly_pubkey_from_pubkey(ctx, &alice_xpk, NULL, &alice_pk);
28 secp256k1_xonly_pubkey_from_pubkey(ctx, &bob_xpk, NULL, &bob_pk);
29 secp256k1_xonly_pubkey_serialize(ctx, alice_xonly, &alice_xpk);
30 secp256k1_xonly_pubkey_serialize(ctx, bob_xonly, &bob_xpk);
31
32 const char *message = "Hello, ContextVM! This is a test message.";
33
34 uint8_t ciphertext[4096];
35 size_t ct_len = 0;
36 nip04_encrypt(alice_sec, bob_xonly, message, ciphertext, &ct_len);
37 ASSERT(ct_len > 0, "encryption produced output");
38 ASSERT(ct_len > strlen(message), "ciphertext longer than plaintext");
39
40 char plaintext[2048];
41 int pt_len = nip04_decrypt(bob_sec, alice_xonly, (const char *)ciphertext, plaintext, sizeof(plaintext));
42 ASSERT(pt_len > 0, "decryption succeeded");
43 ASSERT_EQ_STR(message, plaintext, "decrypted message matches original");
44
45 printf("\n--- Different key produces garbage ---\n");
46 uint8_t eve_sec[32];
47 memset(eve_sec, 0x03, 32);
48 pt_len = nip04_decrypt(eve_sec, alice_xonly, (const char *)ciphertext, plaintext, sizeof(plaintext));
49 if (pt_len > 0) {
50 ASSERT(strcmp(message, plaintext) != 0, "wrong key produces different output");
51 } else {
52 ASSERT(true, "wrong key fails to decrypt");
53 }
54
55 printf("\n--- Second roundtrip (different message) ---\n");
56 const char *msg2 = "Short";
57 nip04_encrypt(alice_sec, bob_xonly, msg2, ciphertext, &ct_len);
58 pt_len = nip04_decrypt(bob_sec, alice_xonly, (const char *)ciphertext, plaintext, sizeof(plaintext));
59 ASSERT(pt_len > 0, "short message decrypts");
60 ASSERT_EQ_STR(msg2, plaintext, "short message matches");
61
62 printf("\n--- Long message roundtrip ---\n");
63 char long_msg[512];
64 memset(long_msg, 'X', 511);
65 long_msg[511] = '\0';
66 nip04_encrypt(alice_sec, bob_xonly, long_msg, ciphertext, &ct_len);
67 pt_len = nip04_decrypt(bob_sec, alice_xonly, (const char *)ciphertext, plaintext, sizeof(plaintext));
68 ASSERT(pt_len > 0, "long message decrypts");
69 ASSERT_EQ_INT(511, pt_len, "long message length matches");
70
71 printf("\n--- Bob encrypts, Alice decrypts ---\n");
72 nip04_encrypt(bob_sec, alice_xonly, message, ciphertext, &ct_len);
73 pt_len = nip04_decrypt(alice_sec, bob_xonly, (const char *)ciphertext, plaintext, sizeof(plaintext));
74 ASSERT(pt_len > 0, "reverse direction decrypts");
75 ASSERT_EQ_STR(message, plaintext, "reverse direction matches");
76
77 secp256k1_context_destroy(ctx);
78}
79
80static void test_nip04_invalid_input(void)
81{
82 printf("\n=== NIP-04 invalid inputs ===\n");
83 char plaintext[256];
84 int rc = nip04_decrypt(NULL, NULL, "AAAA", plaintext, sizeof(plaintext));
85 ASSERT(rc < 0, "NULL keys fails");
86
87 uint8_t dummy_sec[32];
88 memset(dummy_sec, 0xAA, 32);
89 rc = nip04_decrypt(dummy_sec, NULL, "AAAA", plaintext, sizeof(plaintext));
90 ASSERT(rc < 0, "NULL pubkey fails");
91
92 uint8_t dummy_pub[32];
93 memset(dummy_pub, 0xBB, 32);
94 rc = nip04_decrypt(dummy_sec, dummy_pub, "", plaintext, sizeof(plaintext));
95 ASSERT(rc < 0, "empty ciphertext fails");
96
97 rc = nip04_decrypt(dummy_sec, dummy_pub, "AAAA", plaintext, sizeof(plaintext));
98 ASSERT(rc < 0, "garbage ciphertext fails");
99}
100
101int main(void)
102{
103 printf("=== test_nip04 ===\n");
104 test_nip04_roundtrip();
105 test_nip04_invalid_input();
106 TEST_SUMMARY();
107}