upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
Diffstat (limited to 'main')
-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
10 files changed, 699 insertions, 0 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;