ascii-chat 0.6.0
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 "export.h"
8#include "agent.h"
9#include "../keys.h"
10#include "common.h"
11#include "util/string.h"
12#include "util/validation.h"
13#include "log/logging.h"
14#include "platform/system.h"
15
16#include <ctype.h>
17#include <errno.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <string.h>
21#include <unistd.h>
22
23#ifdef _WIN32
24#define SAFE_POPEN _popen
25#define SAFE_PCLOSE _pclose
26#else
27#define SAFE_POPEN popen
28#define SAFE_PCLOSE pclose
29#endif
30
41static int gpg_export_public_key(const char *key_id, uint8_t *public_key_out) {
42 if (!key_id || !public_key_out) {
43 log_error("Invalid arguments to gpg_export_public_key");
44 return -1;
45 }
46
47 // Escape key_id for safe use in shell command
48 char escaped_key_id[BUFFER_SIZE_MEDIUM];
49 if (!escape_shell_single_quotes(key_id, escaped_key_id, sizeof(escaped_key_id))) {
50 log_error("Failed to escape GPG key ID for shell command");
51 return -1;
52 }
53
54 // Create temp file for exported key
55 char temp_path[256];
56 safe_snprintf(temp_path, sizeof(temp_path), "/tmp/asciichat_gpg_export_%d_XXXXXX", getpid());
57 int temp_fd = mkstemp(temp_path);
58 if (temp_fd < 0) {
59 log_error("Failed to create temp file for GPG export: %s", SAFE_STRERROR(errno));
60 return -1;
61 }
62 close(temp_fd);
63
64 // Use gpg --export to export the public key in binary format
65 char cmd[BUFFER_SIZE_LARGE];
66 safe_snprintf(cmd, sizeof(cmd), "gpg --export 0x%s > \"%s\" 2>/dev/null", escaped_key_id, 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 unlink(temp_path);
73 return -1;
74 }
75 log_debug("GPG export completed successfully");
76
77 // Read the exported key file
78 FILE *fp = fopen(temp_path, "rb");
79 if (!fp) {
80 log_error("Failed to open exported GPG key file");
81 unlink(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 unlink(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_info("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_info("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#ifdef _WIN32
282 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-keygrip --with-colons 0x%s 2>nul", escaped_key_id);
283#else
284 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-keygrip --with-colons 0x%s 2>/dev/null", escaped_key_id);
285#endif
286 FILE *fp = SAFE_POPEN(cmd, "r");
287 if (!fp) {
288 log_error("Failed to run gpg command - GPG may not be installed");
289#ifdef _WIN32
290 log_error("To install GPG on Windows, download Gpg4win from:");
291 log_error(" https://www.gpg4win.org/download.html");
292#elif defined(__APPLE__)
293 log_error("To install GPG on macOS, use Homebrew:");
294 log_error(" brew install gnupg");
295#else
296 log_error("To install GPG on Linux:");
297 log_error(" Debian/Ubuntu: sudo apt-get install gnupg");
298 log_error(" Fedora/RHEL: sudo dnf install gnupg2");
299 log_error(" Arch Linux: sudo pacman -S gnupg");
300 log_error(" Alpine Linux: sudo apk add gnupg");
301#endif
302 return -1;
303 }
304
305 char line[BUFFER_SIZE_XLARGE];
306 char found_keygrip[128] = {0};
307 bool found_key = false;
308
309 // Parse gpg output
310 // Format: pub:..., grp:::::::::<keygrip>:
311 while (fgets(line, sizeof(line), fp)) {
312 if (strncmp(line, "pub:", 4) == 0) {
313 // Found the public key line
314 found_key = true;
315 } else if (found_key && strncmp(line, "grp:", 4) == 0) {
316 // Extract keygrip
317 // Format: grp:::::::::D52FF935FBA59609EE65E1685287828242A1EA1A:
318 // (8 empty fields, then keygrip, then final colon)
319 const char *grp_start = line + 4;
320 int colon_count = 0;
321 while (*grp_start && colon_count < 8) {
322 if (*grp_start == ':') {
323 colon_count++;
324 }
325 grp_start++;
326 }
327
328 if (colon_count == 8) {
329 const char *grp_end = strchr(grp_start, ':');
330 if (grp_end) {
331 size_t grp_len = grp_end - grp_start;
332 if (grp_len < sizeof(found_keygrip)) {
333 memcpy(found_keygrip, grp_start, grp_len);
334 found_keygrip[grp_len] = '\0';
335
336 if (keygrip_out) {
337 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
338 }
339 }
340 }
341 }
342 break;
343 }
344 }
345
346 SAFE_PCLOSE(fp);
347
348 if (!found_key || strlen(found_keygrip) == 0) {
349 log_error("Could not find GPG key with ID: %s", key_id);
350 return -1;
351 }
352
353 log_debug("Found keygrip for key %s: %s", key_id, found_keygrip);
354
355 // Try to use GPG agent API to read the public key directly via READKEY command
356 int agent_sock = gpg_agent_connect();
357 if (agent_sock < 0) {
358 log_info("GPG agent not available, falling back to gpg --export for public key extraction");
359 // Fallback: Use gpg --export to get the public key
360 int export_result = gpg_export_public_key(key_id, public_key_out);
361 if (export_result == 0) {
362 log_info("Successfully extracted public key using fallback method");
363 } else {
364 log_error("Fallback public key extraction failed for key ID: %s", key_id);
365 }
366 return export_result;
367 }
368
369 // Send READKEY command with keygrip to get the public key S-expression
370 char readkey_cmd[256];
371 safe_snprintf(readkey_cmd, sizeof(readkey_cmd), "READKEY %s\n", found_keygrip);
372
373 ssize_t bytes_written = platform_pipe_write(agent_sock, (const unsigned char *)readkey_cmd, strlen(readkey_cmd));
374 if (bytes_written != (ssize_t)strlen(readkey_cmd)) {
375 log_error("Failed to send READKEY command to GPG agent");
376 gpg_agent_disconnect(agent_sock);
377 return -1;
378 }
379
380 // Read the response (public key S-expression)
381 char response[BUFFER_SIZE_XXXLARGE];
382 memset(response, 0, sizeof(response));
383 ssize_t bytes_read = platform_pipe_read(agent_sock, (unsigned char *)response, sizeof(response) - 1);
384
385 gpg_agent_disconnect(agent_sock);
386
387 if (bytes_read <= 0) {
388 log_error("Failed to read READKEY response from GPG agent");
389 return -1;
390 }
391
392 // Parse the S-expression to extract Ed25519 public key (q value)
393 // GPG agent returns binary S-expressions in format: (1:q<length>:<binary-data>)
394 // Example: (1:q33:<33-bytes>) where first byte is 0x40 (Ed25519 prefix), then 32-byte key
395 const char *q_marker = strstr(response, "(1:q");
396 if (!q_marker) {
397 log_warn("Failed to find public key (1:q) in GPG agent READKEY response, trying gpg --export fallback");
398 log_debug("Response was: %.*s", (int)(bytes_read < 200 ? bytes_read : 200), response);
399 gpg_agent_disconnect(agent_sock);
400
401 // Fallback: Use gpg --export for public-only keys
402 int export_result = gpg_export_public_key(key_id, public_key_out);
403 if (export_result == 0) {
404 log_info("Successfully extracted public key using gpg --export fallback");
405 } else {
406 log_error("Fallback public key extraction failed for key ID: %s", key_id);
407 }
408 return export_result;
409 }
410
411 // Skip "(1:q" to get to the length field
412 const char *len_start = q_marker + 4;
413
414 // Parse the length (e.g., "33:")
415 char *colon = strchr(len_start, ':');
416 if (!colon) {
417 log_error("Malformed S-expression: missing colon after length");
418 return -1;
419 }
420
421 size_t key_len = strtoul(len_start, NULL, 10);
422 if (key_len != 33) {
423 log_error("Unexpected Ed25519 public key length: %zu bytes (expected 33)", key_len);
424 return -1;
425 }
426
427 // Skip the colon to get to the binary data
428 const unsigned char *binary_start = (const unsigned char *)(colon + 1);
429
430 // Ed25519 public keys in GPG format have a 0x40 prefix byte, then 32 bytes of actual key
431 if (binary_start[0] != 0x40) {
432 log_error("Invalid Ed25519 public key prefix: 0x%02x (expected 0x40)", binary_start[0]);
433 return -1;
434 }
435
436 // Copy the 32-byte public key (skip the 0x40 prefix)
437 memcpy(public_key_out, binary_start + 1, 32);
438
439 log_info("Extracted Ed25519 public key from GPG agent via READKEY command");
440 return 0;
441}
GPG agent connection and communication interface.
#define SAFE_PCLOSE
Definition export.c:28
#define SAFE_POPEN
Definition export.c:27
GPG public key export interface.
#define BUFFER_SIZE_XXXLARGE
Extra extra extra large buffer size (8192 bytes)
#define BUFFER_SIZE_LARGE
Large buffer size (1024 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:358
#define SAFE_STRERROR(errnum)
Definition common.h:385
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:251
int gpg_agent_connect(void)
Connect to gpg-agent.
Definition agent.c:267
void gpg_agent_disconnect(int sock)
Disconnect from gpg-agent.
Definition agent.c:337
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
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 version of snprintf that ensures null termination.
ssize_t platform_pipe_write(pipe_t pipe, const void *buf, size_t len)
Write data to a pipe.
int errno
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)
Definition string.c:101
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
Definition string.c:54
📝 Logging API with multiple log levels and terminal output control
_Atomic uint64_t bytes_written
Definition mmap.c:40
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities
Common validation macros to reduce duplication in protocol handlers.