ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
export.c
Go to the documentation of this file.
1
7#include <ascii-chat/crypto/gpg/export.h>
8#include <ascii-chat/crypto/gpg/agent.h>
9#include <ascii-chat/crypto/keys.h>
10#include <ascii-chat/crypto/regex.h>
11#include <ascii-chat/common.h>
12#include <ascii-chat/util/string.h>
13#include <ascii-chat/util/validation.h>
14#include <ascii-chat/log/logging.h>
15#include <ascii-chat/platform/system.h>
16#include <ascii-chat/platform/filesystem.h>
17#include <ascii-chat/platform/process.h>
18#include <ascii-chat/platform/util.h>
19
20#include <ctype.h>
21#include <errno.h>
22#include <stdio.h>
23#include <stdlib.h>
24#include <string.h>
25#ifndef _WIN32
26#include <unistd.h>
27#endif
28
39static int gpg_export_public_key(const char *key_id, uint8_t *public_key_out) {
40 if (!key_id || !public_key_out) {
41 log_error("Invalid arguments to gpg_export_public_key");
42 return -1;
43 }
44
45 // Escape key_id for safe use in shell command
46 char escaped_key_id[BUFFER_SIZE_MEDIUM];
47 if (!escape_shell_single_quotes(key_id, escaped_key_id, sizeof(escaped_key_id))) {
48 log_error("Failed to escape GPG key ID for shell command");
49 return -1;
50 }
51
52 // Create temp file for exported key
53 char temp_path[PLATFORM_MAX_PATH_LENGTH];
54 int temp_fd = -1;
55 if (platform_create_temp_file(temp_path, sizeof(temp_path), "asciichat_gpg_export", &temp_fd) != 0) {
56 log_error("Failed to create temp file for GPG export");
57 return -1;
58 }
59 if (temp_fd >= 0) {
60 close(temp_fd);
61 }
62
63 // Use gpg --export to export the public key in binary format
64 char cmd[BUFFER_SIZE_LARGE];
65 safe_snprintf(cmd, sizeof(cmd), "gpg --export 0x%s > \"%s\" " PLATFORM_SHELL_NULL_REDIRECT, escaped_key_id,
66 temp_path);
67
68 log_debug("Running GPG export command: gpg --export 0x%s", key_id);
69 int result = system(cmd);
70 if (result != 0) {
71 log_error("Failed to export GPG public key for key ID: %s (exit code: %d)", key_id, result);
72 platform_delete_temp_file(temp_path);
73 return -1;
74 }
75 log_debug("GPG export completed successfully");
76
77 // Read the exported key file
78 FILE *fp = platform_fopen(temp_path, "rb");
79 if (!fp) {
80 log_error("Failed to open exported GPG key file");
81 platform_delete_temp_file(temp_path);
82 return -1;
83 }
84
85 // Read up to 8KB (should be more than enough for a public key packet)
86 uint8_t packet_data[8192];
87 size_t bytes_read = fread(packet_data, 1, sizeof(packet_data), fp);
88 fclose(fp);
89 platform_delete_temp_file(temp_path);
90
91 if (bytes_read == 0) {
92 log_error("GPG export produced empty output - key may not exist");
93 return -1;
94 }
95
96 log_debug("Read %zu bytes from GPG export", bytes_read);
97
98 // Parse OpenPGP packet to extract Ed25519 public key
99 // OpenPGP public key packet format (simplified):
100 // - Packet tag (1 byte): 0x99 for public key packet (old format) or 0xC6 (new format)
101 // - Packet length (variable)
102 // - Version (1 byte): 0x04 for modern keys
103 // - Creation time (4 bytes)
104 // - Algorithm (1 byte): 22 (0x16) for EdDSA
105 // - Curve OID length + OID
106 // - Public key material (MPI format)
107
108 size_t offset = 0;
109
110 // Skip to public key packet (tag 6 - public key, or tag 14 - public subkey)
111 while (offset < bytes_read) {
112 uint8_t tag = packet_data[offset];
113
114 // Check if this is a public key packet (old or new format)
115 bool is_public_key = false;
116 size_t packet_len = 0;
117
118 if ((tag & 0x80) == 0) {
119 // Not a valid packet tag
120 offset++;
121 continue;
122 }
123
124 if ((tag & 0x40) == 0) {
125 // Old format packet
126 uint8_t packet_type = (tag >> 2) & 0x0F;
127 is_public_key = (packet_type == 6 || packet_type == 14); // Public key or subkey
128
129 uint8_t length_type = tag & 0x03;
130 offset++; // Move past tag
131
132 if (length_type == 0) {
133 packet_len = packet_data[offset++];
134 } else if (length_type == 1) {
135 packet_len = (packet_data[offset] << 8) | packet_data[offset + 1];
136 offset += 2;
137 } else if (length_type == 2) {
138 packet_len = (packet_data[offset] << 24) | (packet_data[offset + 1] << 16) | (packet_data[offset + 2] << 8) |
139 packet_data[offset + 3];
140 offset += 4;
141 } else {
142 // Indeterminate length - skip
143 break;
144 }
145 } else {
146 // New format packet
147 uint8_t packet_type = tag & 0x3F;
148 is_public_key = (packet_type == 6 || packet_type == 14); // Public key or subkey
149 offset++; // Move past tag
150
151 // Parse new format length
152 if (offset >= bytes_read)
153 break;
154 uint8_t first_len = packet_data[offset++];
155
156 if (first_len < 192) {
157 packet_len = first_len;
158 } else if (first_len < 224) {
159 if (offset >= bytes_read)
160 break;
161 packet_len = ((first_len - 192) << 8) + packet_data[offset++] + 192;
162 } else if (first_len == 255) {
163 if (offset + 4 > bytes_read)
164 break;
165 packet_len = (packet_data[offset] << 24) | (packet_data[offset + 1] << 16) | (packet_data[offset + 2] << 8) |
166 packet_data[offset + 3];
167 offset += 4;
168 } else {
169 // Partial body length - not expected for key packets
170 break;
171 }
172 }
173
174 if (!is_public_key || packet_len == 0 || offset + packet_len > bytes_read) {
175 offset += packet_len;
176 continue;
177 }
178
179 // Parse the public key packet content
180 size_t packet_start = offset;
181
182 // Check version (should be 4)
183 if (packet_data[offset] != 0x04) {
184 offset += packet_len;
185 continue;
186 }
187 offset++; // Skip version
188
189 offset += 4; // Skip creation time
190
191 // Check algorithm (22 = EdDSA/Ed25519)
192 if (offset >= packet_start + packet_len) {
193 offset = packet_start + packet_len;
194 continue;
195 }
196
197 uint8_t algorithm = packet_data[offset++];
198 if (algorithm != 22) { // Not EdDSA
199 offset = packet_start + packet_len;
200 continue;
201 }
202
203 // Skip curve OID (should be Ed25519 OID)
204 if (offset >= packet_start + packet_len) {
205 offset = packet_start + packet_len;
206 continue;
207 }
208
209 uint8_t oid_len = packet_data[offset++];
210 offset += oid_len; // Skip OID bytes
211
212 // Now we should have the MPI-encoded public key
213 // MPI format: 2-byte bit count, then key data
214 if (offset + 2 > packet_start + packet_len) {
215 offset = packet_start + packet_len;
216 continue;
217 }
218
219 uint16_t mpi_bits = (packet_data[offset] << 8) | packet_data[offset + 1];
220 offset += 2;
221
222 // Ed25519 public keys should be 263 bits (0x0107) - includes 0x40 prefix byte
223 // Or 256 bits for just the key without prefix
224 size_t mpi_bytes = (mpi_bits + 7) / 8;
225
226 if (offset + mpi_bytes > packet_start + packet_len) {
227 offset = packet_start + packet_len;
228 continue;
229 }
230
231 // Ed25519 keys in OpenPGP have a 0x40 prefix byte
232 if (mpi_bytes == 33 && packet_data[offset] == 0x40) {
233 // Found it! Extract the 32-byte public key (skip 0x40 prefix)
234 memcpy(public_key_out, &packet_data[offset + 1], 32);
235 log_debug("Extracted Ed25519 public key from gpg --export (fallback method)");
236 return 0;
237 } else if (mpi_bytes == 32) {
238 // Key without prefix (less common but valid)
239 memcpy(public_key_out, &packet_data[offset], 32);
240 log_debug("Extracted Ed25519 public key from gpg --export (fallback method)");
241 return 0;
242 }
243
244 offset = packet_start + packet_len;
245 }
246
247 log_error("Failed to find Ed25519 public key in GPG export data");
248 return -1;
249}
250
251int gpg_get_public_key(const char *key_id, uint8_t *public_key_out, char *keygrip_out) {
252 if (!key_id || !public_key_out) {
253 log_error("Invalid arguments to gpg_get_public_key");
254 return -1;
255 }
256
257 // SECURITY: Validate key_id to prevent command injection
258 // GPG key IDs should be hexadecimal (0-9, a-f, A-F)
259 if (!validate_shell_safe(key_id, NULL)) {
260 log_error("Invalid GPG key ID format - contains unsafe characters: %s", key_id);
261 return -1;
262 }
263
264 // Additional validation: ensure key_id is hex alphanumeric
265 for (size_t i = 0; key_id[i] != '\0'; i++) {
266 if (!isxdigit((unsigned char)key_id[i])) {
267 log_error("Invalid GPG key ID format - must be hexadecimal: %s", key_id);
268 return -1;
269 }
270 }
271
272 // Escape key_id for safe use in shell command (single quotes)
273 char escaped_key_id[BUFFER_SIZE_MEDIUM];
274 if (!escape_shell_single_quotes(key_id, escaped_key_id, sizeof(escaped_key_id))) {
275 log_error("Failed to escape GPG key ID for shell command");
276 return -1;
277 }
278
279 // Use gpg to list the key and get the keygrip
280 char cmd[BUFFER_SIZE_LARGE];
281 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-keygrip --with-colons 0x%s " PLATFORM_SHELL_NULL_REDIRECT,
282 escaped_key_id);
283
284 FILE *fp = NULL;
285 if (platform_popen(cmd, "r", &fp) != ASCIICHAT_OK || !fp) {
286 log_error("Failed to run gpg command - GPG may not be installed");
287#ifdef _WIN32
288 log_error("To install GPG on Windows, download Gpg4win from:");
289 log_error(" https://www.gpg4win.org/download.html");
290#elif defined(__APPLE__)
291 log_error("To install GPG on macOS, use Homebrew:");
292 log_error(" brew install gnupg");
293#else
294 log_error("To install GPG on Linux:");
295 log_error(" Debian/Ubuntu: sudo apt-get install gnupg");
296 log_error(" Fedora/RHEL: sudo dnf install gnupg2");
297 log_error(" Arch Linux: sudo pacman -S gnupg");
298 log_error(" Alpine Linux: sudo apk add gnupg");
299#endif
300 return -1;
301 }
302
303 char line[BUFFER_SIZE_XLARGE];
304 char found_keygrip[128] = {0};
305 bool found_key = false;
306
307 // Parse gpg output
308 // Format: pub:..., grp:::::::::<keygrip>:
309 while (fgets(line, sizeof(line), fp)) {
310 if (strncmp(line, "pub:", 4) == 0) {
311 // Found the public key line
312 found_key = true;
313 } else if (found_key && strncmp(line, "grp:", 4) == 0) {
314 // Extract keygrip using PCRE2 regex
315 // Format: grp:::::::::D52FF935FBA59609EE65E1685287828242A1EA1A:
316 char *keygrip_extracted = NULL;
317
318 if (crypto_regex_extract_gpg_keygrip(line, &keygrip_extracted)) {
319 // Successfully extracted keygrip
320 SAFE_STRNCPY(found_keygrip, keygrip_extracted, sizeof(found_keygrip));
321 if (keygrip_out) {
322 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
323 }
324 SAFE_FREE(keygrip_extracted);
325 } else {
326 // Fallback to manual parsing if regex fails
327 const char *grp_start = line + 4;
328 int colon_count = 0;
329 while (*grp_start && colon_count < 8) {
330 if (*grp_start == ':') {
331 colon_count++;
332 }
333 grp_start++;
334 }
335
336 if (colon_count == 8) {
337 const char *grp_end = strchr(grp_start, ':');
338 if (grp_end) {
339 size_t grp_len = grp_end - grp_start;
340 if (grp_len < sizeof(found_keygrip)) {
341 memcpy(found_keygrip, grp_start, grp_len);
342 found_keygrip[grp_len] = '\0';
343
344 if (keygrip_out) {
345 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
346 }
347 }
348 }
349 }
350 }
351 break;
352 }
353 }
354
355 platform_pclose(&fp);
356
357 if (!found_key || strlen(found_keygrip) == 0) {
358 log_error("Could not find GPG key with ID: %s", key_id);
359 return -1;
360 }
361
362 log_debug("Found keygrip for key %s: %s", key_id, found_keygrip);
363
364 // Try to use GPG agent API to read the public key directly via READKEY command
365 int agent_sock = gpg_agent_connect();
366 if (agent_sock < 0) {
367 log_debug("GPG agent not available, falling back to gpg --export for public key extraction");
368 // Fallback: Use gpg --export to get the public key
369 int export_result = gpg_export_public_key(key_id, public_key_out);
370 if (export_result == 0) {
371 log_debug("Successfully extracted public key using fallback method");
372 } else {
373 log_error("Fallback public key extraction failed for key ID: %s", key_id);
374 }
375 return export_result;
376 }
377
378 // Send READKEY command with keygrip to get the public key S-expression
379 char readkey_cmd[BUFFER_SIZE_SMALL];
380 safe_snprintf(readkey_cmd, sizeof(readkey_cmd), "READKEY %s\n", found_keygrip);
381
382 // Cast agent_sock back to pipe_t (gpg_agent_connect returns pipe_t cast to int for portability)
383 pipe_t agent_pipe = (pipe_t)(intptr_t)agent_sock;
384 ssize_t bytes_written = platform_pipe_write(agent_pipe, (const unsigned char *)readkey_cmd, strlen(readkey_cmd));
385 if (bytes_written != (ssize_t)strlen(readkey_cmd)) {
386 log_error("Failed to send READKEY command to GPG agent");
387 gpg_agent_disconnect(agent_sock);
388 return -1;
389 }
390
391 // Read the response (public key S-expression)
392 char response[BUFFER_SIZE_XXXLARGE];
393 memset(response, 0, sizeof(response));
394 ssize_t bytes_read = platform_pipe_read(agent_pipe, (unsigned char *)response, sizeof(response) - 1);
395
396 gpg_agent_disconnect(agent_sock);
397
398 if (bytes_read <= 0) {
399 log_error("Failed to read READKEY response from GPG agent");
400 return -1;
401 }
402
403 // Parse the S-expression to extract Ed25519 public key (q value)
404 // GPG agent returns binary S-expressions in format: (1:q<length>:<binary-data>)
405 // Example: (1:q33:<33-bytes>) where first byte is 0x40 (Ed25519 prefix), then 32-byte key
406 const char *q_marker = strstr(response, "(1:q");
407 if (!q_marker) {
408 log_warn("Failed to find public key (1:q) in GPG agent READKEY response, trying gpg --export fallback");
409 log_debug("Response was: %.*s", (int)(bytes_read < 200 ? bytes_read : 200), response);
410 gpg_agent_disconnect(agent_sock);
411
412 // Fallback: Use gpg --export for public-only keys
413 int export_result = gpg_export_public_key(key_id, public_key_out);
414 if (export_result == 0) {
415 log_debug("Successfully extracted public key using gpg --export fallback");
416 } else {
417 log_error("Fallback public key extraction failed for key ID: %s", key_id);
418 }
419 return export_result;
420 }
421
422 // Skip "(1:q" to get to the length field
423 const char *len_start = q_marker + 4;
424
425 // Parse the length (e.g., "33:")
426 char *colon = strchr(len_start, ':');
427 if (!colon) {
428 log_error("Malformed S-expression: missing colon after length");
429 return -1;
430 }
431
432 size_t key_len = strtoul(len_start, NULL, 10);
433 if (key_len != 33) {
434 log_error("Unexpected Ed25519 public key length: %zu bytes (expected 33)", key_len);
435 return -1;
436 }
437
438 // Skip the colon to get to the binary data
439 const unsigned char *binary_start = (const unsigned char *)(colon + 1);
440
441 // Ed25519 public keys in GPG format have a 0x40 prefix byte, then 32 bytes of actual key
442 if (binary_start[0] != 0x40) {
443 log_error("Invalid Ed25519 public key prefix: 0x%02x (expected 0x40)", binary_start[0]);
444 return -1;
445 }
446
447 // Copy the 32-byte public key (skip the 0x40 prefix)
448 memcpy(public_key_out, binary_start + 1, 32);
449
450 log_debug("Extracted Ed25519 public key from GPG agent via READKEY command");
451 return 0;
452}
int gpg_agent_connect(void)
Definition agent.c:96
void gpg_agent_disconnect(int handle_as_int)
Definition agent.c:146
int gpg_get_public_key(const char *key_id, uint8_t *public_key_out, char *keygrip_out)
Definition export.c:251
_Atomic uint64_t bytes_written
Definition mmap.c:42
bool crypto_regex_extract_gpg_keygrip(const char *line, char **keygrip_out)
Definition regex.c:267
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
bool validate_shell_safe(const char *str, const char *allowed_chars)
Definition util/string.c:59
FILE * platform_fopen(const char *filename, const char *mode)