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>
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");
46 char escaped_key_id[BUFFER_SIZE_MEDIUM];
48 log_error(
"Failed to escape GPG key ID for shell command");
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");
64 char cmd[BUFFER_SIZE_LARGE];
65 safe_snprintf(cmd,
sizeof(cmd),
"gpg --export 0x%s > \"%s\" " PLATFORM_SHELL_NULL_REDIRECT, escaped_key_id,
68 log_debug(
"Running GPG export command: gpg --export 0x%s", key_id);
69 int result = system(cmd);
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);
75 log_debug(
"GPG export completed successfully");
80 log_error(
"Failed to open exported GPG key file");
81 platform_delete_temp_file(temp_path);
86 uint8_t packet_data[8192];
87 size_t bytes_read = fread(packet_data, 1,
sizeof(packet_data), fp);
89 platform_delete_temp_file(temp_path);
91 if (bytes_read == 0) {
92 log_error(
"GPG export produced empty output - key may not exist");
96 log_debug(
"Read %zu bytes from GPG export", bytes_read);
111 while (offset < bytes_read) {
112 uint8_t tag = packet_data[offset];
115 bool is_public_key =
false;
116 size_t packet_len = 0;
118 if ((tag & 0x80) == 0) {
124 if ((tag & 0x40) == 0) {
126 uint8_t packet_type = (tag >> 2) & 0x0F;
127 is_public_key = (packet_type == 6 || packet_type == 14);
129 uint8_t length_type = tag & 0x03;
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];
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];
147 uint8_t packet_type = tag & 0x3F;
148 is_public_key = (packet_type == 6 || packet_type == 14);
152 if (offset >= bytes_read)
154 uint8_t first_len = packet_data[offset++];
156 if (first_len < 192) {
157 packet_len = first_len;
158 }
else if (first_len < 224) {
159 if (offset >= bytes_read)
161 packet_len = ((first_len - 192) << 8) + packet_data[offset++] + 192;
162 }
else if (first_len == 255) {
163 if (offset + 4 > bytes_read)
165 packet_len = (packet_data[offset] << 24) | (packet_data[offset + 1] << 16) | (packet_data[offset + 2] << 8) |
166 packet_data[offset + 3];
174 if (!is_public_key || packet_len == 0 || offset + packet_len > bytes_read) {
175 offset += packet_len;
180 size_t packet_start = offset;
183 if (packet_data[offset] != 0x04) {
184 offset += packet_len;
192 if (offset >= packet_start + packet_len) {
193 offset = packet_start + packet_len;
197 uint8_t algorithm = packet_data[offset++];
198 if (algorithm != 22) {
199 offset = packet_start + packet_len;
204 if (offset >= packet_start + packet_len) {
205 offset = packet_start + packet_len;
209 uint8_t oid_len = packet_data[offset++];
214 if (offset + 2 > packet_start + packet_len) {
215 offset = packet_start + packet_len;
219 uint16_t mpi_bits = (packet_data[offset] << 8) | packet_data[offset + 1];
224 size_t mpi_bytes = (mpi_bits + 7) / 8;
226 if (offset + mpi_bytes > packet_start + packet_len) {
227 offset = packet_start + packet_len;
232 if (mpi_bytes == 33 && packet_data[offset] == 0x40) {
234 memcpy(public_key_out, &packet_data[offset + 1], 32);
235 log_debug(
"Extracted Ed25519 public key from gpg --export (fallback method)");
237 }
else if (mpi_bytes == 32) {
239 memcpy(public_key_out, &packet_data[offset], 32);
240 log_debug(
"Extracted Ed25519 public key from gpg --export (fallback method)");
244 offset = packet_start + packet_len;
247 log_error(
"Failed to find Ed25519 public key in GPG export data");
252 if (!key_id || !public_key_out) {
253 log_error(
"Invalid arguments to gpg_get_public_key");
260 log_error(
"Invalid GPG key ID format - contains unsafe characters: %s", key_id);
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);
273 char escaped_key_id[BUFFER_SIZE_MEDIUM];
275 log_error(
"Failed to escape GPG key ID for shell command");
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,
285 if (platform_popen(cmd,
"r", &fp) != ASCIICHAT_OK || !fp) {
286 log_error(
"Failed to run gpg command - GPG may not be installed");
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");
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");
303 char line[BUFFER_SIZE_XLARGE];
304 char found_keygrip[128] = {0};
305 bool found_key =
false;
309 while (fgets(line,
sizeof(line), fp)) {
310 if (strncmp(line,
"pub:", 4) == 0) {
313 }
else if (found_key && strncmp(line,
"grp:", 4) == 0) {
316 char *keygrip_extracted = NULL;
320 SAFE_STRNCPY(found_keygrip, keygrip_extracted,
sizeof(found_keygrip));
322 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
324 SAFE_FREE(keygrip_extracted);
327 const char *grp_start = line + 4;
329 while (*grp_start && colon_count < 8) {
330 if (*grp_start ==
':') {
336 if (colon_count == 8) {
337 const char *grp_end = strchr(grp_start,
':');
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';
345 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
355 platform_pclose(&fp);
357 if (!found_key || strlen(found_keygrip) == 0) {
358 log_error(
"Could not find GPG key with ID: %s", key_id);
362 log_debug(
"Found keygrip for key %s: %s", key_id, found_keygrip);
366 if (agent_sock < 0) {
367 log_debug(
"GPG agent not available, falling back to gpg --export for public key extraction");
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");
373 log_error(
"Fallback public key extraction failed for key ID: %s", key_id);
375 return export_result;
379 char readkey_cmd[BUFFER_SIZE_SMALL];
380 safe_snprintf(readkey_cmd,
sizeof(readkey_cmd),
"READKEY %s\n", found_keygrip);
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));
386 log_error(
"Failed to send READKEY command to GPG agent");
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);
398 if (bytes_read <= 0) {
399 log_error(
"Failed to read READKEY response from GPG agent");
406 const char *q_marker = strstr(response,
"(1:q");
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);
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");
417 log_error(
"Fallback public key extraction failed for key ID: %s", key_id);
419 return export_result;
423 const char *len_start = q_marker + 4;
426 char *colon = strchr(len_start,
':');
428 log_error(
"Malformed S-expression: missing colon after length");
432 size_t key_len = strtoul(len_start, NULL, 10);
434 log_error(
"Unexpected Ed25519 public key length: %zu bytes (expected 33)", key_len);
439 const unsigned char *binary_start = (
const unsigned char *)(colon + 1);
442 if (binary_start[0] != 0x40) {
443 log_error(
"Invalid Ed25519 public key prefix: 0x%02x (expected 0x40)", binary_start[0]);
448 memcpy(public_key_out, binary_start + 1, 32);
450 log_debug(
"Extracted Ed25519 public key from GPG agent via READKEY command");
int gpg_agent_connect(void)
void gpg_agent_disconnect(int handle_as_int)
int gpg_get_public_key(const char *key_id, uint8_t *public_key_out, char *keygrip_out)
_Atomic uint64_t bytes_written
bool crypto_regex_extract_gpg_keygrip(const char *line, char **keygrip_out)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
#define PLATFORM_MAX_PATH_LENGTH
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)
FILE * platform_fopen(const char *filename, const char *mode)