upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/nip04.c
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 /main/nip04.c
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
Diffstat (limited to 'main/nip04.c')
-rw-r--r--main/nip04.c201
1 files changed, 201 insertions, 0 deletions
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}