ascii-chat 0.8.38
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 <ascii-chat/crypto/gpg/agent.h>
8#include <ascii-chat/crypto/keys.h>
9#include <ascii-chat/common.h>
10#include <ascii-chat/util/string.h>
11#include <ascii-chat/log/logging.h>
12#include <ascii-chat/platform/system.h>
13#include <ascii-chat/platform/agent.h>
14#include <ascii-chat/platform/pipe.h>
15
16#include <ctype.h>
17#include <errno.h>
18#include <sodium.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22
23#ifndef _WIN32
24#include <sys/socket.h>
25#include <sys/un.h>
26#include <unistd.h>
27#endif
28
29// Maximum response size from gpg-agent
30#define GPG_AGENT_MAX_RESPONSE 8192
31
36static int read_agent_line(pipe_t pipe, char *buf, size_t buf_size) {
37 size_t pos = 0;
38 while (pos < buf_size - 1) {
39 char c;
40 ssize_t n = platform_pipe_read(pipe, &c, 1);
41 if (n <= 0) {
42 if (n == 0) {
43 log_error("GPG agent connection closed");
44 } else {
45 log_error("Error reading from GPG agent");
46 }
47 return -1;
48 }
49
50 if (c == '\n') {
51 buf[pos] = '\0';
52 return 0;
53 }
54
55 buf[pos++] = c;
56 }
57
58 log_error("GPG agent response too long");
59 return -1;
60}
61
65static int send_agent_command(pipe_t pipe, const char *command) {
66 size_t len = strlen(command);
67 char *cmd_with_newline;
68 cmd_with_newline = SAFE_MALLOC(len + 2, char *);
69 if (!cmd_with_newline) {
70 log_error("Failed to allocate memory for command");
71 return -1;
72 }
73
74 memcpy(cmd_with_newline, command, len);
75 cmd_with_newline[len] = '\n';
76 cmd_with_newline[len + 1] = '\0';
77
78 ssize_t sent = platform_pipe_write(pipe, cmd_with_newline, len + 1);
79 SAFE_FREE(cmd_with_newline);
80
81 if (sent != (ssize_t)(len + 1)) {
82 log_error("Failed to send command to GPG agent");
83 return -1;
84 }
85
86 return 0;
87}
88
92static bool is_ok_response(const char *line) {
93 return strncmp(line, "OK", 2) == 0;
94}
95
97 char agent_path[PLATFORM_MAX_PATH_LENGTH];
98 if (platform_get_gpg_agent_socket(agent_path, sizeof(agent_path)) != 0) {
99 log_error("Failed to get GPG agent path");
100 return -1;
101 }
102
103 log_debug("Connecting to GPG agent at: %s", agent_path);
104
105 // Use platform abstraction for pipe/socket connection
106 pipe_t pipe = platform_pipe_connect(agent_path);
107 if (!platform_pipe_is_valid(pipe)) {
108 log_error("Failed to connect to GPG agent");
109 return -1;
110 }
111
112 // Read initial greeting
113 char response[GPG_AGENT_MAX_RESPONSE];
114 if (read_agent_line(pipe, response, sizeof(response)) != 0) {
115 log_error("Failed to read GPG agent greeting");
116 platform_pipe_close(pipe);
117 return -1;
118 }
119
120 if (!is_ok_response(response)) {
121 log_error("Unexpected GPG agent greeting: %s", response);
122 platform_pipe_close(pipe);
123 return -1;
124 }
125
126 log_debug("Connected to GPG agent successfully");
127
128 // Set loopback pinentry mode to avoid interactive prompts
129 // This allows GPG agent to work in non-interactive environments
130 if (send_agent_command(pipe, "OPTION pinentry-mode=loopback") != 0) {
131 log_warn("Failed to set loopback pinentry mode (continuing anyway)");
132 } else {
133 // Read response for OPTION command
134 if (read_agent_line(pipe, response, sizeof(response)) != 0) {
135 log_warn("Failed to read OPTION command response (continuing anyway)");
136 } else if (is_ok_response(response)) {
137 log_debug("Loopback pinentry mode enabled");
138 } else {
139 log_warn("Failed to enable loopback pinentry mode: %s (continuing anyway)", response);
140 }
141 }
142
143 return (int)(intptr_t)pipe;
144}
145
146void gpg_agent_disconnect(int handle_as_int) {
147 if (handle_as_int >= 0) {
148 pipe_t pipe = (pipe_t)(intptr_t)handle_as_int;
149 send_agent_command(pipe, "BYE");
150 platform_pipe_close(pipe);
151 }
152}
153
154int gpg_agent_sign(int handle_as_int, const char *keygrip, const uint8_t *message, size_t message_len,
155 uint8_t *signature_out, size_t *signature_len_out) {
156 if (handle_as_int < 0 || !keygrip || !message || !signature_out || !signature_len_out) {
157 log_error("Invalid arguments to gpg_agent_sign");
158 return -1;
159 }
160
161 pipe_t handle = (pipe_t)(intptr_t)handle_as_int;
162 char response[GPG_AGENT_MAX_RESPONSE];
163
164 // 1. Set the key to use (SIGKEY command)
165 char sigkey_cmd[128];
166 safe_snprintf(sigkey_cmd, sizeof(sigkey_cmd), "SIGKEY %s", keygrip);
167 if (send_agent_command(handle, sigkey_cmd) != 0) {
168 log_error("Failed to send SIGKEY command");
169 return -1;
170 }
171
172 if (read_agent_line(handle, response, sizeof(response)) != 0) {
173 log_error("Failed to read SIGKEY response");
174 return -1;
175 }
176
177 if (!is_ok_response(response)) {
178 log_error("SIGKEY failed: %s", response);
179 return -1;
180 }
181
182 // 2. For EdDSA/Ed25519, GPG agent requires SETHASH with a hash algorithm
183 // GPG agent doesn't support --inquire for SETHASH - the command syntax is:
184 // SETHASH (--hash=<name>)|(<algonumber>) <hexstring>
185 // For Ed25519, we hash the message with SHA512 (algo 10) first
186
187 // Hash the message with SHA512 using libsodium
188 uint8_t hash[crypto_hash_sha512_BYTES];
189 crypto_hash_sha512(hash, message, message_len);
190
191 // Build SETHASH command with SHA512 hash (algo 10)
192 // Format: "SETHASH 10 <128 hex chars for 64-byte SHA512 hash>"
193 char sethash_cmd[256];
194 int offset = safe_snprintf(sethash_cmd, sizeof(sethash_cmd), "SETHASH 10 ");
195 for (size_t i = 0; i < crypto_hash_sha512_BYTES; i++) {
196 offset += safe_snprintf(sethash_cmd + offset, sizeof(sethash_cmd) - (size_t)offset, "%02X", hash[i]);
197 }
198
199 log_debug("Sending SETHASH command with SHA512 hash");
200 if (send_agent_command(handle, sethash_cmd) != 0) {
201 log_error("Failed to send SETHASH command");
202 return -1;
203 }
204
205 // Read SETHASH response
206 if (read_agent_line(handle, response, sizeof(response)) != 0) {
207 log_error("Failed to read SETHASH response");
208 return -1;
209 }
210
211 if (!is_ok_response(response)) {
212 log_error("SETHASH failed: %s", response);
213 return -1;
214 }
215
216 // 3. Request signature using PKSIGN
217 if (send_agent_command(handle, "PKSIGN") != 0) {
218 log_error("Failed to send PKSIGN command");
219 return -1;
220 }
221
222 // Read response - skip status/error lines and wait for data line (D ...)
223 // GPG agent sends informational ERR lines that are not fatal (e.g., "Not implemented")
224 // Keep reading until we get the actual signature data
225 bool found_data = false;
226 for (int attempts = 0; attempts < 20; attempts++) {
227 if (read_agent_line(handle, response, sizeof(response)) != 0) {
228 log_error("Failed to read PKSIGN response");
229 return -1;
230 }
231
232 log_debug("PKSIGN response line %d: %s", attempts + 1, response);
233
234 // Skip status lines (S INQUIRE_MAXLEN, etc)
235 if (response[0] == 'S' && response[1] == ' ') {
236 log_debug("Skipping PKSIGN status line: %s", response);
237 continue;
238 }
239
240 // Skip informational ERR lines (GPG agent sends these even on success)
241 // Common ERR codes: 67109141 (IPC cancelled), 67108933 (Not implemented)
242 if (strncmp(response, "ERR", 3) == 0) {
243 log_debug("Skipping PKSIGN error line (informational): %s", response);
244 continue;
245 }
246
247 // Check if it's a data line (D followed by space)
248 if (response[0] == 'D' && response[1] == ' ') {
249 log_debug("Found signature data line");
250 found_data = true;
251 break;
252 }
253
254 // Check for OK (success without data would be unexpected)
255 if (strncmp(response, "OK", 2) == 0) {
256 log_warn("PKSIGN returned OK without data line");
257 continue; // Keep trying in case D line follows
258 }
259
260 // Check if GPG agent is sending another INQUIRE (shouldn't happen)
261 if (strncmp(response, "INQUIRE", 7) == 0) {
262 log_error("Unexpected INQUIRE after PKSIGN: %s", response);
263 return -1;
264 }
265
266 // Unknown response type
267 log_warn("Unexpected PKSIGN response (attempt %d): %s", attempts + 1, response);
268 }
269
270 if (!found_data) {
271 log_error("Expected D line from PKSIGN after %d attempts", 20);
272 return -1;
273 }
274
275 // Parse S-expression signature from GPG agent
276 // GPG agent returns: D <percent-encoded-sexp>
277 // Example: D (7:sig-val(5:eddsa(1:r32:%<hex>)(1:s32:%<hex>)))
278 // The signature is 64 bytes total: R (32) + S (32)
279
280 // DEBUG: Print first 200 chars of response to see format
281 char debug_buf[201];
282 size_t response_len = strlen(response);
283 size_t debug_len = response_len < 200 ? response_len : 200;
284 memcpy(debug_buf, response, debug_len);
285 debug_buf[debug_len] = '\0';
286 log_debug("GPG agent D line (first 200 bytes): %s", debug_buf);
287
288 const char *data = response + 2; // Skip "D "
289
290 // The response format from GPG agent for EdDSA is percent-encoded
291 // We need to decode it to get the raw binary signature
292 // For now, let's try the simple approach: find the raw data
293
294 // Look for the pattern that indicates where R starts: "(1:r32:"
295 const char *r_marker = strstr(data, "(1:r32:");
296 if (!r_marker) {
297 log_error("Could not find r value marker in S-expression");
298 return -1;
299 }
300
301 // Skip the marker to get to the actual R data
302 const char *r_data = r_marker + 7; // strlen("(1:r32:")
303
304 // Look for the pattern that indicates where S starts: "(1:s32:"
305 const char *s_marker = strstr(r_data + 32, "(1:s32:");
306 if (!s_marker) {
307 log_error("Could not find s value marker in S-expression");
308 return -1;
309 }
310
311 // Skip the marker to get to the actual S data
312 const char *s_data = s_marker + 7; // strlen("(1:s32:")
313
314 // Copy the raw binary data
315 memcpy(signature_out, r_data, 32);
316 memcpy(signature_out + 32, s_data, 32);
317
318 *signature_len_out = 64;
319
320 // DEBUG: Print signature in hex
321 char sig_hex[129];
322 for (int i = 0; i < 64; i++) {
323 safe_snprintf(sig_hex + i * 2, 3, "%02x", (unsigned char)signature_out[i]);
324 }
325 sig_hex[128] = '\0';
326 log_debug("Extracted signature (64 bytes): %s", sig_hex);
327
328 // Read final OK
329 if (read_agent_line(handle, response, sizeof(response)) != 0) {
330 log_error("Failed to read final PKSIGN response");
331 return -1;
332 }
333
334 if (!is_ok_response(response)) {
335 log_error("PKSIGN final response not OK: %s", response);
336 return -1;
337 }
338
339 log_debug("Successfully signed message with GPG agent");
340 return 0;
341}
342
344 int sock = gpg_agent_connect();
345 if (sock < 0) {
346 return false;
347 }
349 return true;
350}
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)
Definition agent.c:154
bool gpg_agent_is_available(void)
Definition agent.c:343
int gpg_agent_connect(void)
Definition agent.c:96
#define GPG_AGENT_MAX_RESPONSE
Definition agent.c:30
void gpg_agent_disconnect(int handle_as_int)
Definition agent.c:146
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