ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
agent.c
Go to the documentation of this file.
1
7#include "agent.h"
8#include "../keys.h"
9#include "common.h"
10#include "util/string.h"
11#include "log/logging.h"
12#include "platform/system.h"
13
14#include <ctype.h>
15#include <errno.h>
16#include <sodium.h>
17#include <stdio.h>
18#include <stdlib.h>
19#include <string.h>
20
21#ifdef _WIN32
22#include <windows.h>
23#define SAFE_POPEN _popen
24#define SAFE_PCLOSE _pclose
25#else
26#include <sys/socket.h>
27#include <sys/un.h>
28#include <unistd.h>
29#define SAFE_POPEN popen
30#define SAFE_PCLOSE pclose
31#endif
32
33// Maximum response size from gpg-agent
34#define GPG_AGENT_MAX_RESPONSE 8192
35
39static int get_agent_socket_path(char *path_out, size_t path_size) {
40#ifdef _WIN32
41 // On Windows, GPG4Win uses a named pipe
42 // Try gpgconf first to get the correct path
43 FILE *fp = SAFE_POPEN("gpgconf --list-dirs agent-socket 2>nul", "r");
44 if (fp) {
45 if (fgets(path_out, path_size, fp)) {
46 // Remove trailing newline
47 size_t len = strlen(path_out);
48 if (len > 0 && path_out[len - 1] == '\n') {
49 path_out[len - 1] = '\0';
50 }
51 SAFE_PCLOSE(fp);
52 return 0;
53 }
54 SAFE_PCLOSE(fp);
55 }
56
57 // Fallback to default GPG4Win location
58 const char *appdata = SAFE_GETENV("APPDATA");
59 if (appdata) {
60 safe_snprintf(path_out, path_size, "%s\\gnupg\\S.gpg-agent", appdata);
61 } else {
62 log_error("Could not determine APPDATA directory");
63 return -1;
64 }
65#else
66 // Try gpgconf first
67 FILE *fp = SAFE_POPEN("gpgconf --list-dirs agent-socket 2>/dev/null", "r");
68 if (fp) {
69 if (fgets(path_out, path_size, fp)) {
70 // Remove trailing newline
71 size_t len = strlen(path_out);
72 if (len > 0 && path_out[len - 1] == '\n') {
73 path_out[len - 1] = '\0';
74 }
75 SAFE_PCLOSE(fp);
76 return 0;
77 }
78 SAFE_PCLOSE(fp);
79 }
80
81 // Fallback to default location
82 const char *gnupg_home = SAFE_GETENV("GNUPGHOME");
83 if (gnupg_home) {
84 safe_snprintf(path_out, path_size, "%s/S.gpg-agent", gnupg_home);
85 } else {
86 const char *home = SAFE_GETENV("HOME");
87 if (!home) {
88 log_error("Could not determine home directory");
89 return -1;
90 }
91 safe_snprintf(path_out, path_size, "%s/.gnupg/S.gpg-agent", home);
92 }
93#endif
94
95 return 0;
96}
97
102#ifdef _WIN32
103static int read_agent_line(HANDLE pipe, char *buf, size_t buf_size) {
104 size_t pos = 0;
105 while (pos < buf_size - 1) {
106 char c;
107 DWORD bytes_read;
108 if (!ReadFile(pipe, &c, 1, &bytes_read, NULL) || bytes_read != 1) {
109 if (GetLastError() == ERROR_BROKEN_PIPE) {
110 log_error("GPG agent connection closed");
111 } else {
112 log_error("Error reading from GPG agent: %lu", GetLastError());
113 }
114 return -1;
115 }
116
117 if (c == '\n') {
118 buf[pos] = '\0';
119 return 0;
120 }
121
122 buf[pos++] = c;
123 }
124
125 log_error("GPG agent response too long");
126 return -1;
127}
128#else
129static int read_agent_line(int sock, char *buf, size_t buf_size) {
130 size_t pos = 0;
131 while (pos < buf_size - 1) {
132 char c;
133 ssize_t n = recv(sock, &c, 1, 0);
134 if (n <= 0) {
135 if (n == 0) {
136 log_error("GPG agent connection closed");
137 } else {
138 log_error("Error reading from GPG agent: %s", SAFE_STRERROR(errno));
139 }
140 return -1;
141 }
142
143 if (c == '\n') {
144 buf[pos] = '\0';
145 return 0;
146 }
147
148 buf[pos++] = c;
149 }
150
151 log_error("GPG agent response too long");
152 return -1;
153}
154#endif
155
159#ifdef _WIN32
160static int send_agent_command(HANDLE pipe, const char *command) {
161 size_t len = strlen(command);
162 char *cmd_with_newline;
163 cmd_with_newline = SAFE_MALLOC(len + 2, char *);
164 if (!cmd_with_newline) {
165 log_error("Failed to allocate memory for command");
166 return -1;
167 }
168
169 memcpy(cmd_with_newline, command, len);
170 cmd_with_newline[len] = '\n';
171 cmd_with_newline[len + 1] = '\0';
172
173 DWORD bytes_written;
174 BOOL result = WriteFile(pipe, cmd_with_newline, (DWORD)(len + 1), &bytes_written, NULL);
175 SAFE_FREE(cmd_with_newline);
176
177 if (!result || bytes_written != (len + 1)) {
178 log_error("Failed to send command to GPG agent: %lu", GetLastError());
179 return -1;
180 }
181
182 return 0;
183}
184#else
185static int send_agent_command(int sock, const char *command) {
186 size_t len = strlen(command);
187 char *cmd_with_newline;
188 cmd_with_newline = SAFE_MALLOC(len + 2, char *);
189 if (!cmd_with_newline) {
190 log_error("Failed to allocate memory for command");
191 return -1;
192 }
193
194 memcpy(cmd_with_newline, command, len);
195 cmd_with_newline[len] = '\n';
196 cmd_with_newline[len + 1] = '\0';
197
198 ssize_t sent = send(sock, cmd_with_newline, len + 1, 0);
199 SAFE_FREE(cmd_with_newline);
200
201 if (sent != (ssize_t)(len + 1)) {
202 log_error("Failed to send command to GPG agent");
203 return -1;
204 }
205
206 return 0;
207}
208#endif
209
213static bool is_ok_response(const char *line) {
214 return strncmp(line, "OK", 2) == 0;
215}
216
217#ifdef _WIN32
218// On Windows, we return HANDLE cast to int (handle values are always even on Windows)
219int gpg_agent_connect(void) {
220 char pipe_path[PLATFORM_MAX_PATH_LENGTH];
221 if (get_agent_socket_path(pipe_path, sizeof(pipe_path)) != 0) {
222 log_error("Failed to get GPG agent pipe path");
223 return -1;
224 }
225
226 log_debug("Connecting to GPG agent at: %s", pipe_path);
227
228 // Wait for pipe to be available (gpg-agent may take time to start)
229 if (!WaitNamedPipeA(pipe_path, 5000)) {
230 log_error("GPG agent pipe not available: %lu", GetLastError());
231 return -1;
232 }
233
234 HANDLE pipe = CreateFileA(pipe_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
235
236 if (pipe == INVALID_HANDLE_VALUE) {
237 log_error("Failed to connect to GPG agent pipe: %lu", GetLastError());
238 return -1;
239 }
240
241 // Set pipe to message mode
242 DWORD mode = PIPE_READMODE_BYTE;
243 if (!SetNamedPipeHandleState(pipe, &mode, NULL, NULL)) {
244 log_error("Failed to set pipe mode: %lu", GetLastError());
245 CloseHandle(pipe);
246 return -1;
247 }
248
249 // Read initial greeting
250 char response[GPG_AGENT_MAX_RESPONSE];
251 if (read_agent_line(pipe, response, sizeof(response)) != 0) {
252 log_error("Failed to read GPG agent greeting");
253 CloseHandle(pipe);
254 return -1;
255 }
256
257 if (!is_ok_response(response)) {
258 log_error("Unexpected GPG agent greeting: %s", response);
259 CloseHandle(pipe);
260 return -1;
261 }
262
263 log_debug("Connected to GPG agent successfully");
264 return (int)(intptr_t)pipe;
265}
266#else
268 char socket_path[PLATFORM_MAX_PATH_LENGTH];
269 if (get_agent_socket_path(socket_path, sizeof(socket_path)) != 0) {
270 log_error("Failed to get GPG agent socket path");
271 return -1;
272 }
273
274 log_debug("Connecting to GPG agent at: %s", socket_path);
275
276 int sock = socket(AF_UNIX, SOCK_STREAM, 0);
277 if (sock < 0) {
278 log_error("Failed to create socket: %s", SAFE_STRERROR(errno));
279 return -1;
280 }
281
282 struct sockaddr_un addr;
283 memset(&addr, 0, sizeof(addr));
284 addr.sun_family = AF_UNIX;
285 SAFE_STRNCPY(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
286
287 if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
288 log_error("Failed to connect to GPG agent: %s", SAFE_STRERROR(errno));
289 close(sock);
290 return -1;
291 }
292
293 // Read initial greeting
294 char response[GPG_AGENT_MAX_RESPONSE];
295 if (read_agent_line(sock, response, sizeof(response)) != 0) {
296 log_error("Failed to read GPG agent greeting");
297 close(sock);
298 return -1;
299 }
300
301 if (!is_ok_response(response)) {
302 log_error("Unexpected GPG agent greeting: %s", response);
303 close(sock);
304 return -1;
305 }
306
307 log_debug("Connected to GPG agent successfully");
308
309 // Set loopback pinentry mode to avoid interactive prompts
310 // This allows GPG agent to work in non-interactive environments
311 if (send_agent_command(sock, "OPTION pinentry-mode=loopback") != 0) {
312 log_warn("Failed to set loopback pinentry mode (continuing anyway)");
313 } else {
314 // Read response for OPTION command
315 if (read_agent_line(sock, response, sizeof(response)) != 0) {
316 log_warn("Failed to read OPTION command response (continuing anyway)");
317 } else if (is_ok_response(response)) {
318 log_debug("Loopback pinentry mode enabled");
319 } else {
320 log_warn("Failed to enable loopback pinentry mode: %s (continuing anyway)", response);
321 }
322 }
323
324 return sock;
325}
326#endif
327
328#ifdef _WIN32
329void gpg_agent_disconnect(int handle_as_int) {
330 if (handle_as_int >= 0) {
331 HANDLE pipe = (HANDLE)(intptr_t)handle_as_int;
332 send_agent_command(pipe, "BYE");
333 CloseHandle(pipe);
334 }
335}
336#else
337void gpg_agent_disconnect(int sock) {
338 if (sock >= 0) {
339 send_agent_command(sock, "BYE");
340 close(sock);
341 }
342}
343#endif
344
345int gpg_agent_sign(int handle_as_int, const char *keygrip, const uint8_t *message, size_t message_len,
346 uint8_t *signature_out, size_t *signature_len_out) {
347 if (handle_as_int < 0 || !keygrip || !message || !signature_out || !signature_len_out) {
348 log_error("Invalid arguments to gpg_agent_sign");
349 return -1;
350 }
351
352#ifdef _WIN32
353 HANDLE handle = (HANDLE)(intptr_t)handle_as_int;
354#else
355 int handle = handle_as_int;
356#endif
357
358 char response[GPG_AGENT_MAX_RESPONSE];
359
360 // 1. Set the key to use (SIGKEY command)
361 char sigkey_cmd[128];
362 safe_snprintf(sigkey_cmd, sizeof(sigkey_cmd), "SIGKEY %s", keygrip);
363 if (send_agent_command(handle, sigkey_cmd) != 0) {
364 log_error("Failed to send SIGKEY command");
365 return -1;
366 }
367
368 if (read_agent_line(handle, response, sizeof(response)) != 0) {
369 log_error("Failed to read SIGKEY response");
370 return -1;
371 }
372
373 if (!is_ok_response(response)) {
374 log_error("SIGKEY failed: %s", response);
375 return -1;
376 }
377
378 // 2. For EdDSA/Ed25519, GPG agent requires SETHASH with a hash algorithm
379 // GPG agent doesn't support --inquire for SETHASH - the command syntax is:
380 // SETHASH (--hash=<name>)|(<algonumber>) <hexstring>
381 // For Ed25519, we hash the message with SHA512 (algo 10) first
382
383 // Hash the message with SHA512 using libsodium
384 uint8_t hash[crypto_hash_sha512_BYTES];
385 crypto_hash_sha512(hash, message, message_len);
386
387 // Build SETHASH command with SHA512 hash (algo 10)
388 // Format: "SETHASH 10 <128 hex chars for 64-byte SHA512 hash>"
389 char sethash_cmd[256];
390 int offset = safe_snprintf(sethash_cmd, sizeof(sethash_cmd), "SETHASH 10 ");
391 for (size_t i = 0; i < crypto_hash_sha512_BYTES; i++) {
392 offset += safe_snprintf(sethash_cmd + offset, sizeof(sethash_cmd) - (size_t)offset, "%02X", hash[i]);
393 }
394
395 log_debug("Sending SETHASH command with SHA512 hash");
396 if (send_agent_command(handle, sethash_cmd) != 0) {
397 log_error("Failed to send SETHASH command");
398 return -1;
399 }
400
401 // Read SETHASH response
402 if (read_agent_line(handle, response, sizeof(response)) != 0) {
403 log_error("Failed to read SETHASH response");
404 return -1;
405 }
406
407 if (!is_ok_response(response)) {
408 log_error("SETHASH failed: %s", response);
409 return -1;
410 }
411
412 // 3. Request signature using PKSIGN
413 if (send_agent_command(handle, "PKSIGN") != 0) {
414 log_error("Failed to send PKSIGN command");
415 return -1;
416 }
417
418 // Read response - skip status/error lines and wait for data line (D ...)
419 // GPG agent sends informational ERR lines that are not fatal (e.g., "Not implemented")
420 // Keep reading until we get the actual signature data
421 bool found_data = false;
422 for (int attempts = 0; attempts < 20; attempts++) {
423 if (read_agent_line(handle, response, sizeof(response)) != 0) {
424 log_error("Failed to read PKSIGN response");
425 return -1;
426 }
427
428 log_debug("PKSIGN response line %d: %s", attempts + 1, response);
429
430 // Skip status lines (S INQUIRE_MAXLEN, etc)
431 if (response[0] == 'S' && response[1] == ' ') {
432 log_debug("Skipping PKSIGN status line: %s", response);
433 continue;
434 }
435
436 // Skip informational ERR lines (GPG agent sends these even on success)
437 // Common ERR codes: 67109141 (IPC cancelled), 67108933 (Not implemented)
438 if (strncmp(response, "ERR", 3) == 0) {
439 log_debug("Skipping PKSIGN error line (informational): %s", response);
440 continue;
441 }
442
443 // Check if it's a data line (D followed by space)
444 if (response[0] == 'D' && response[1] == ' ') {
445 log_debug("Found signature data line");
446 found_data = true;
447 break;
448 }
449
450 // Check for OK (success without data would be unexpected)
451 if (strncmp(response, "OK", 2) == 0) {
452 log_warn("PKSIGN returned OK without data line");
453 continue; // Keep trying in case D line follows
454 }
455
456 // Check if GPG agent is sending another INQUIRE (shouldn't happen)
457 if (strncmp(response, "INQUIRE", 7) == 0) {
458 log_error("Unexpected INQUIRE after PKSIGN: %s", response);
459 return -1;
460 }
461
462 // Unknown response type
463 log_warn("Unexpected PKSIGN response (attempt %d): %s", attempts + 1, response);
464 }
465
466 if (!found_data) {
467 log_error("Expected D line from PKSIGN after %d attempts", 20);
468 return -1;
469 }
470
471 // Parse S-expression signature from GPG agent
472 // GPG agent returns: D <percent-encoded-sexp>
473 // Example: D (7:sig-val(5:eddsa(1:r32:%<hex>)(1:s32:%<hex>)))
474 // The signature is 64 bytes total: R (32) + S (32)
475
476 // DEBUG: Print first 200 chars of response to see format
477 char debug_buf[201];
478 size_t response_len = strlen(response);
479 size_t debug_len = response_len < 200 ? response_len : 200;
480 memcpy(debug_buf, response, debug_len);
481 debug_buf[debug_len] = '\0';
482 log_debug("GPG agent D line (first 200 bytes): %s", debug_buf);
483
484 const char *data = response + 2; // Skip "D "
485
486 // The response format from GPG agent for EdDSA is percent-encoded
487 // We need to decode it to get the raw binary signature
488 // For now, let's try the simple approach: find the raw data
489
490 // Look for the pattern that indicates where R starts: "(1:r32:"
491 const char *r_marker = strstr(data, "(1:r32:");
492 if (!r_marker) {
493 log_error("Could not find r value marker in S-expression");
494 return -1;
495 }
496
497 // Skip the marker to get to the actual R data
498 const char *r_data = r_marker + 7; // strlen("(1:r32:")
499
500 // Look for the pattern that indicates where S starts: "(1:s32:"
501 const char *s_marker = strstr(r_data + 32, "(1:s32:");
502 if (!s_marker) {
503 log_error("Could not find s value marker in S-expression");
504 return -1;
505 }
506
507 // Skip the marker to get to the actual S data
508 const char *s_data = s_marker + 7; // strlen("(1:s32:")
509
510 // Copy the raw binary data
511 memcpy(signature_out, r_data, 32);
512 memcpy(signature_out + 32, s_data, 32);
513
514 *signature_len_out = 64;
515
516 // DEBUG: Print signature in hex
517 char sig_hex[129];
518 for (int i = 0; i < 64; i++) {
519 safe_snprintf(sig_hex + i * 2, 3, "%02x", (unsigned char)signature_out[i]);
520 }
521 sig_hex[128] = '\0';
522 log_debug("Extracted signature (64 bytes): %s", sig_hex);
523
524 // Read final OK
525 if (read_agent_line(handle, response, sizeof(response)) != 0) {
526 log_error("Failed to read final PKSIGN response");
527 return -1;
528 }
529
530 if (!is_ok_response(response)) {
531 log_error("PKSIGN final response not OK: %s", response);
532 return -1;
533 }
534
535 log_debug("Successfully signed message with GPG agent");
536 return 0;
537}
538
540 int sock = gpg_agent_connect();
541 if (sock < 0) {
542 return false;
543 }
545 return true;
546}
#define SAFE_PCLOSE
Definition agent.c:30
#define GPG_AGENT_MAX_RESPONSE
Definition agent.c:34
#define SAFE_POPEN
Definition agent.c:29
GPG agent connection and communication interface.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_GETENV(name)
Definition common.h:378
#define SAFE_STRERROR(errnum)
Definition common.h:385
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:91
unsigned char uint8_t
Definition common.h:56
bool gpg_agent_is_available(void)
Check if GPG agent is available.
Definition agent.c:539
int gpg_agent_connect(void)
Connect to gpg-agent.
Definition agent.c:267
int gpg_agent_sign(int handle_as_int, const char *keygrip, const uint8_t *message, size_t message_len, uint8_t *signature_out, size_t *signature_len_out)
Sign a message using GPG agent.
Definition agent.c:345
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_debug(...)
Log a DEBUG message.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
int errno
📝 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