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