ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
openpgp.c
Go to the documentation of this file.
1
9#include <ascii-chat/common.h>
11#include <ascii-chat/log/log.h>
12#include <ascii-chat/log/io.h>
16#include <string.h>
17#include <stdlib.h>
18#include <sodium.h>
19#ifndef _WIN32
20#include <unistd.h>
21#include <sys/types.h>
22#include <sys/stat.h>
23#else
24#include <sys/types.h>
25#include <sys/stat.h>
26#endif
27
28// =============================================================================
29// Base64 Decoding for PGP Armor
30// =============================================================================
31
32asciichat_error_t openpgp_base64_decode(const char *base64, size_t base64_len, uint8_t **binary_out,
33 size_t *binary_len) {
34 if (!base64 || !binary_out || !binary_len) {
35 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for base64 decode");
36 }
37
38 // Remove whitespace from base64 input (PGP armor has newlines)
39 char *clean_base64 = SAFE_MALLOC(base64_len + 1, char *);
40 char *clean_ptr = clean_base64;
41 for (size_t i = 0; i < base64_len; i++) {
42 if (base64[i] != '\n' && base64[i] != '\r' && base64[i] != ' ' && base64[i] != '\t') {
43 *clean_ptr++ = base64[i];
44 }
45 }
46 *clean_ptr = '\0';
47 size_t clean_len = (size_t)(clean_ptr - clean_base64);
48
49 // Allocate max possible output size
50 *binary_out = SAFE_MALLOC(clean_len, uint8_t *);
51
52 const char *end;
53 int result = sodium_base642bin(*binary_out, clean_len, clean_base64, clean_len, NULL, binary_len, &end,
54 sodium_base64_VARIANT_ORIGINAL);
55
56 SAFE_FREE(clean_base64);
57
58 if (result != 0) {
59 SAFE_FREE(*binary_out);
60 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode base64 PGP armored data");
61 }
62
63 return ASCIICHAT_OK;
64}
65
66// =============================================================================
67// OpenPGP Packet Header Parsing
68// =============================================================================
69
71 if (!data || !header || data_len == 0) {
72 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for packet header parsing");
73 }
74
75 memset(header, 0, sizeof(openpgp_packet_header_t));
76
77 uint8_t ctb = data[0]; // Cipher Type Byte
78
79 // Check if bit 7 is set (all packets must have bit 7 = 1)
80 if ((ctb & 0x80) == 0) {
81 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenPGP packet: bit 7 not set in CTB");
82 }
83
84 // Check if new format (bit 6 = 1) or old format (bit 6 = 0)
85 if (ctb & 0x40) {
86 // New format: bits 5-0 = tag
87 header->new_format = true;
88 header->tag = ctb & 0x3F;
89
90 if (data_len < 2) {
91 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for new format packet header");
92 }
93
94 uint8_t len_byte = data[1];
95
96 if (len_byte < 192) {
97 // One-octet length
98 header->length = len_byte;
99 header->header_len = 2;
100 } else if (len_byte < 224) {
101 // Two-octet length
102 if (data_len < 3) {
103 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for two-octet length");
104 }
105 header->length = ((len_byte - 192) << 8) + data[2] + 192;
106 header->header_len = 3;
107 } else if (len_byte == 255) {
108 // Five-octet length
109 if (data_len < 6) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for five-octet length");
111 }
112 header->length = ((size_t)data[2] << 24) | ((size_t)data[3] << 16) | ((size_t)data[4] << 8) | data[5];
113 header->header_len = 6;
114 } else {
115 // Partial body length (not supported for our use case)
116 return SET_ERRNO(ERROR_CRYPTO_KEY, "Partial body length not supported");
117 }
118 } else {
119 // Old format: bits 5-2 = tag, bits 1-0 = length type
120 header->new_format = false;
121 header->tag = (ctb >> 2) & 0x0F;
122 uint8_t length_type = ctb & 0x03;
123
124 switch (length_type) {
125 case 0: // One-octet length
126 if (data_len < 2) {
127 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for one-octet length");
128 }
129 header->length = data[1];
130 header->header_len = 2;
131 break;
132
133 case 1: // Two-octet length
134 if (data_len < 3) {
135 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for two-octet length");
136 }
137 header->length = ((size_t)data[1] << 8) | data[2];
138 header->header_len = 3;
139 break;
140
141 case 2: // Four-octet length
142 if (data_len < 5) {
143 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for four-octet length");
144 }
145 header->length = ((size_t)data[1] << 24) | ((size_t)data[2] << 16) | ((size_t)data[3] << 8) | data[4];
146 header->header_len = 5;
147 break;
148
149 case 3: // Indeterminate length (not supported)
150 return SET_ERRNO(ERROR_CRYPTO_KEY, "Indeterminate length not supported");
151
152 default:
153 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid length type: %u", length_type);
154 }
155 }
156
157 log_debug("OpenPGP packet: tag=%u, length=%zu, header_len=%zu, new_format=%d", header->tag, header->length,
158 header->header_len, header->new_format);
159
160 return ASCIICHAT_OK;
161}
162
163// =============================================================================
164// MPI (Multi-Precision Integer) Parsing
165// =============================================================================
166
167asciichat_error_t openpgp_extract_ed25519_from_mpi(const uint8_t *mpi, size_t mpi_len, uint8_t ed25519_pk[32]) {
168 if (!mpi || !ed25519_pk || mpi_len < 35) {
169 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for MPI extraction (need at least 35 bytes)");
170 }
171
172 // MPI format:
173 // - 2 bytes: bit count (big-endian)
174 // - 1 byte: 0x40 prefix (Ed25519 marker)
175 // - 32 bytes: Ed25519 public key
176
177 uint16_t bit_count = ((uint16_t)mpi[0] << 8) | mpi[1];
178 log_debug("MPI bit count: %u", bit_count);
179
180 // Ed25519 with 0x40 prefix is typically 263 bits (0x0107)
181 if (bit_count < 256 || bit_count > 270) {
182 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unexpected MPI bit count for Ed25519: %u (expected ~263)", bit_count);
183 }
184
185 // Check for 0x40 prefix byte
186 if (mpi[2] != 0x40) {
187 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing 0x40 prefix byte in Ed25519 MPI (found 0x%02x)", mpi[2]);
188 }
189
190 // Extract 32-byte Ed25519 public key
191 memcpy(ed25519_pk, mpi + 3, 32);
192
193 return ASCIICHAT_OK;
194}
195
196// =============================================================================
197// Public Key Packet Parsing
198// =============================================================================
199
201 openpgp_public_key_t *pubkey) {
202 if (!packet_body || !pubkey || body_len < 6) {
203 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key packet parsing");
204 }
205
206 memset(pubkey, 0, sizeof(openpgp_public_key_t));
207
208 size_t offset = 0;
209
210 // Version (1 byte, must be 4)
211 pubkey->version = packet_body[offset++];
212 if (pubkey->version != 4) {
213 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP public key version: %u (only version 4 supported)",
214 pubkey->version);
215 }
216
217 // Creation time (4 bytes, big-endian Unix timestamp)
218 pubkey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
219 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
220 offset += 4;
221
222 // Algorithm (1 byte)
223 pubkey->algorithm = packet_body[offset++];
224
225 log_debug("Public key packet: version=%u, created=%u, algorithm=%u", pubkey->version, pubkey->created,
226 pubkey->algorithm);
227
228 // Only support EdDSA (algorithm 22)
229 if (pubkey->algorithm != OPENPGP_ALGO_EDDSA) {
230 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported public key algorithm: %u (only EdDSA/22 supported)",
231 pubkey->algorithm);
232 }
233
234 // EdDSA (Ed25519) keys have a special encoding:
235 // - OID for the curve (variable length)
236 // - 0x40 prefix byte
237 // - 32 bytes of Ed25519 public key
238 //
239 // We search for the 0x40 prefix and extract the following 32 bytes
240
241 // Search for 0x40 prefix byte in the remaining packet data
242 bool found_prefix = false;
243 size_t key_offset = 0;
244
245 for (size_t i = offset; i < body_len - 32; i++) {
246 if (packet_body[i] == 0x40) {
247 key_offset = i + 1; // Point to first byte after 0x40
248 found_prefix = true;
249 log_debug("Found Ed25519 key prefix 0x40 at offset %zu", i);
250 break;
251 }
252 }
253
254 if (!found_prefix) {
255 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in packet");
256 }
257
258 if (key_offset + 32 > body_len) {
259 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
260 }
261
262 // Extract the 32-byte Ed25519 public key
263 memcpy(pubkey->pubkey, packet_body + key_offset, 32);
264
265 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", pubkey->pubkey[0],
266 pubkey->pubkey[1], pubkey->pubkey[2], pubkey->pubkey[3], pubkey->pubkey[4], pubkey->pubkey[5],
267 pubkey->pubkey[6], pubkey->pubkey[7]);
268
269 // Calculate Key ID (last 8 bytes of SHA-1 fingerprint)
270 // For now, we'll skip fingerprint calculation and just extract from packet if available
271 // The keyid is typically at a fixed offset for Ed25519 keys
272 // We'll compute it properly by hashing the public key material
273
274 // For version 4 keys, fingerprint = SHA-1(0x99 || length || packet_body)
275 // Key ID = last 8 bytes of fingerprint
276 // For simplicity, we'll set keyid to 0 for now (not critical for our use case)
277 pubkey->keyid = 0;
278
279 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", pubkey->pubkey[0],
280 pubkey->pubkey[1], pubkey->pubkey[2], pubkey->pubkey[3], pubkey->pubkey[4], pubkey->pubkey[5],
281 pubkey->pubkey[6], pubkey->pubkey[7]);
282
283 return ASCIICHAT_OK;
284}
285
286// =============================================================================
287// PGP Armored Format Parsing
288// =============================================================================
289
290asciichat_error_t openpgp_parse_armored_pubkey(const char *armored_text, uint8_t ed25519_pk[32]) {
291 if (!armored_text || !ed25519_pk) {
292 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored pubkey parsing");
293 }
294
295 // Find the BEGIN marker
296 const char *begin = strstr(armored_text, "-----BEGIN PGP PUBLIC KEY BLOCK-----");
297 if (!begin) {
298 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PUBLIC KEY BLOCK BEGIN marker");
299 }
300
301 // Skip to the end of the BEGIN line
302 const char *base64_start = strchr(begin, '\n');
303 if (!base64_start) {
304 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
305 }
306 base64_start++; // Skip the newline
307
308 // Find the END marker
309 const char *end = strstr(base64_start, "-----END PGP PUBLIC KEY BLOCK-----");
310 if (!end) {
311 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PUBLIC KEY BLOCK END marker");
312 }
313
314 // Find the checksum line (starts with '=') and exclude it
315 const char *base64_end = end;
316 const char *checksum = base64_end;
317 while (checksum > base64_start && *checksum != '=') {
318 checksum--;
319 }
320 if (*checksum == '=') {
321 // Move back to before the checksum line
322 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
323 checksum--;
324 }
325 base64_end = checksum;
326 }
327
328 size_t base64_len = (size_t)(base64_end - base64_start);
329
330 log_debug("Extracting base64 data from PGP armor (%zu bytes)", base64_len);
331
332 // Decode base64 to binary OpenPGP packets
333 uint8_t *binary_data;
334 size_t binary_len;
335 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
336 if (decode_result != ASCIICHAT_OK) {
337 return decode_result;
338 }
339
340 log_debug("Decoded %zu bytes of OpenPGP packet data", binary_len);
341
342 // Parse OpenPGP packets to find the public key packet (tag 6)
343 size_t offset = 0;
344 bool found_pubkey = false;
345
346 while (offset < binary_len) {
348 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
349 if (header_result != ASCIICHAT_OK) {
350 SAFE_FREE(binary_data);
351 return header_result;
352 }
353
354 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
355
356 // Check if this is a public key packet (tag 6)
357 if (header.tag == OPENPGP_TAG_PUBLIC_KEY) {
359 asciichat_error_t parse_result =
360 openpgp_parse_public_key_packet(binary_data + offset + header.header_len, header.length, &pubkey);
361
362 if (parse_result == ASCIICHAT_OK) {
363 memcpy(ed25519_pk, pubkey.pubkey, 32);
364 found_pubkey = true;
365 log_debug("Extracted Ed25519 public key from OpenPGP armored block");
366 break;
367 } else {
368 // Not an Ed25519 key, try next packet
369 log_debug("Skipping non-Ed25519 public key packet");
370 }
371 }
372
373 // Move to next packet
374 offset += header.header_len + header.length;
375 }
376
377 SAFE_FREE(binary_data);
378
379 if (!found_pubkey) {
380 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 public key found in PGP armored block");
381 }
382
383 return ASCIICHAT_OK;
384}
385
386// =============================================================================
387// Secret Key Packet Parsing
388// =============================================================================
389
391 openpgp_secret_key_t *seckey) {
392 if (!packet_body || !seckey || body_len < 6) {
393 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for secret key packet parsing");
394 }
395
396 memset(seckey, 0, sizeof(openpgp_secret_key_t));
397
398 size_t offset = 0;
399
400 // Parse public key portion (same as public key packet)
401 // Version (1 byte, must be 4)
402 seckey->version = packet_body[offset++];
403 if (seckey->version != 4) {
404 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP secret key version: %u (only version 4 supported)",
405 seckey->version);
406 }
407
408 // Creation time (4 bytes, big-endian Unix timestamp)
409 seckey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
410 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
411 offset += 4;
412
413 // Algorithm (1 byte)
414 seckey->algorithm = packet_body[offset++];
415
416 log_debug("Secret key packet: version=%u, created=%u, algorithm=%u", seckey->version, seckey->created,
417 seckey->algorithm);
418
419 // Only support EdDSA (algorithm 22)
420 if (seckey->algorithm != OPENPGP_ALGO_EDDSA) {
421 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported secret key algorithm: %u (only EdDSA/22 supported)",
422 seckey->algorithm);
423 }
424
425 // EdDSA public key: OID + 0x40 prefix + 32 bytes of Ed25519 public key
426 // Search for 0x40 prefix byte
427 bool found_prefix = false;
428 size_t pubkey_offset = 0;
429
430 for (size_t i = offset; i < body_len - 32; i++) {
431 if (packet_body[i] == 0x40) {
432 pubkey_offset = i + 1;
433 found_prefix = true;
434 log_debug("Found Ed25519 public key prefix 0x40 at offset %zu", i);
435 break;
436 }
437 }
438
439 if (!found_prefix) {
440 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in secret key packet");
441 }
442
443 if (pubkey_offset + 32 > body_len) {
444 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
445 }
446
447 // Extract the 32-byte Ed25519 public key
448 memcpy(seckey->pubkey, packet_body + pubkey_offset, 32);
449
450 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", seckey->pubkey[0],
451 seckey->pubkey[1], seckey->pubkey[2], seckey->pubkey[3], seckey->pubkey[4], seckey->pubkey[5],
452 seckey->pubkey[6], seckey->pubkey[7]);
453
454 // Move offset past public key material
455 offset = pubkey_offset + 32;
456
457 // S2K usage byte (1 byte)
458 // 0x00 = secret key is not encrypted
459 // 0xFE or 0xFF = secret key is encrypted with S2K
460 if (offset >= body_len) {
461 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing S2K usage byte in secret key packet");
462 }
463
464 uint8_t s2k_usage = packet_body[offset++];
465 log_debug("S2K usage byte: 0x%02x", s2k_usage);
466
467 if (s2k_usage != 0x00) {
468 seckey->is_encrypted = true;
469 log_debug("Detected encrypted secret key (S2K usage = 0x%02x)", s2k_usage);
470 // Don't parse encrypted key material here - caller will need to decrypt with gpg
471 return ASCIICHAT_OK;
472 }
473
474 seckey->is_encrypted = false;
475
476 // For unencrypted keys (S2K usage = 0x00), secret key material follows directly
477 // For Ed25519: 32 bytes of secret key
478 if (offset + 32 > body_len) {
479 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 secret key (need 32 bytes)");
480 }
481
482 // Extract the 32-byte Ed25519 secret key
483 memcpy(seckey->seckey, packet_body + offset, 32);
484
485 log_debug("Extracted Ed25519 secret key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", seckey->seckey[0],
486 seckey->seckey[1], seckey->seckey[2], seckey->seckey[3], seckey->seckey[4], seckey->seckey[5],
487 seckey->seckey[6], seckey->seckey[7]);
488
489 return ASCIICHAT_OK;
490}
491
504static asciichat_error_t openpgp_decrypt_with_gpg(const char *armored_text, char **decrypted_out) {
505 if (!armored_text || !decrypted_out) {
506 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for GPG decryption");
507 }
508
509 // Get passphrase from environment variable or interactive prompt
510 const char *passphrase = SAFE_GETENV("ASCII_CHAT_KEY_PASSWORD");
511 char passphrase_buffer[512] = {0};
512
513 if (!passphrase) {
514 // No environment variable - try interactive prompt
516 log_info("GPG key is encrypted - prompting for passphrase");
517 if (platform_prompt_question("Enter passphrase for GPG key", passphrase_buffer, sizeof(passphrase_buffer),
518 PROMPT_OPTS_PASSWORD) != 0) {
519 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to read passphrase for encrypted GPG key");
520 }
521 passphrase = passphrase_buffer;
522 } else {
524 "Encrypted GPG key requires passphrase. Set ASCII_CHAT_KEY_PASSWORD environment variable or "
525 "run interactively.");
526 }
527 }
528
529 // Create a temporary GPG homedir (isolated from user's keyring)
531 if (!homedir) {
532 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
533 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to create temporary GPG homedir");
534 }
535
536 // Create temporary files for input (armored key) and output (decrypted key)
537 char input_path[PLATFORM_MAX_PATH_LENGTH];
538 int input_fd = -1;
539 if (platform_create_temp_file(input_path, sizeof(input_path), "asc_gpg_in", &input_fd) != 0) {
540 gpg_homedir_destroy(homedir);
541 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
542 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to create temporary input file");
543 }
544
545#ifdef _WIN32
546 // On Windows, platform_create_temp_file returns fd=-1, need to open separately
547 input_fd = platform_open("gpg_input_file", input_path, O_WRONLY | O_BINARY);
548 if (input_fd < 0) {
549 platform_delete_temp_file(input_path);
550 gpg_homedir_destroy(homedir);
551 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
552 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open temporary input file");
553 }
554#endif
555
556 // Write armored key to temp file
557 size_t armored_len = strlen(armored_text);
558 ssize_t written = write(input_fd, armored_text, armored_len);
559 close(input_fd);
560
561 if (written != (ssize_t)armored_len) {
562 platform_unlink(input_path);
563 gpg_homedir_destroy(homedir);
564 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
565 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to write armored key to temp file");
566 }
567
568 // Create temporary output file for decrypted key
569 char output_path[PLATFORM_MAX_PATH_LENGTH];
570 int output_fd = -1;
571 if (platform_create_temp_file(output_path, sizeof(output_path), "asc_gpg_out", &output_fd) != 0) {
572 platform_unlink(input_path);
573 gpg_homedir_destroy(homedir);
574 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
575 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to create temporary output file");
576 }
577 if (output_fd >= 0) {
578 close(output_fd);
579 }
580
581 // Use temporary homedir for isolation instead of shell pipeline
582 const char *homedir_path = gpg_homedir_path(homedir);
583
584 // Step 1: Import the key into the temporary homedir
585 log_debug("Importing GPG key into temporary homedir");
586 const char *argv_import[] = {"gpg", "--homedir", homedir_path, "--batch", "--import", input_path, NULL};
587 int status = 0;
588 LOG_IO("gpg", { status = platform_execute_subprocess("gpg", argv_import, NULL, 0); });
589
590 // Clean up input file (no longer needed)
591 platform_unlink(input_path);
592
593 if (status != 0) {
594 platform_unlink(output_path);
595 gpg_homedir_destroy(homedir);
596 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
597 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to import GPG key into temporary homedir");
598 }
599
600 // Step 2: Get the key fingerprint from the temporary homedir
601 log_debug("Retrieving GPG key fingerprint");
602 char fpr_output[4096];
603 const char *argv_list[] = {"gpg", "--homedir", homedir_path, "--batch", "--list-secret-keys", "--with-colons", NULL};
604 LOG_IO("gpg", { status = platform_execute_subprocess("gpg", argv_list, fpr_output, sizeof(fpr_output)); });
605
606 if (status != 0) {
607 platform_unlink(output_path);
608 gpg_homedir_destroy(homedir);
609 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
610 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to retrieve GPG key fingerprint");
611 }
612
613 // Parse fingerprint from output: find line starting with "fpr" and extract field 10
614 char key_fpr[41] = {0}; // Ed25519 fingerprints are 40 hex chars
615 const char *line = fpr_output;
616 while (*line) {
617 if (strncmp(line, "fpr:", 4) == 0) {
618 // Found fpr line, extract the fingerprint (field 10, colon-separated)
619 int field_count = 0;
620 const char *field_start = line;
621 for (const char *p = line; *p && *p != '\n'; p++) {
622 if (*p == ':') {
623 field_count++;
624 field_start = p + 1;
625 if (field_count == 10) {
626 // Found field 10, copy until next colon or newline
627 size_t fpr_len = 0;
628 while (field_start[fpr_len] && field_start[fpr_len] != ':' && field_start[fpr_len] != '\n' &&
629 fpr_len < sizeof(key_fpr) - 1) {
630 fpr_len++;
631 }
632 if (fpr_len > 0 && fpr_len < sizeof(key_fpr)) {
633 safe_snprintf(key_fpr, sizeof(key_fpr), "%.*s", (int)fpr_len, field_start);
634 }
635 break;
636 }
637 }
638 }
639 break;
640 }
641 // Skip to next line
642 while (*line && *line != '\n')
643 line++;
644 if (*line == '\n')
645 line++;
646 }
647
648 if (!key_fpr[0]) {
649 log_error("Could not find GPG key fingerprint in output");
650 platform_unlink(output_path);
651 gpg_homedir_destroy(homedir);
652 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
653 return SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key fingerprint not found in keyring");
654 }
655
656 log_debug("Found GPG key fingerprint: %s", key_fpr);
657
658 // Step 3: Export the secret key using the fingerprint with passphrase
659 log_debug("Exporting decrypted GPG key");
660 const char *argv_export[] = {"gpg",
661 "--homedir",
662 homedir_path,
663 "--batch",
664 "--pinentry-mode",
665 "loopback",
666 "--passphrase",
667 passphrase,
668 "--armor",
669 "--export-secret-keys",
670 "--export-options",
671 "export-minimal,no-export-attributes",
672 "--output",
673 output_path,
674 key_fpr,
675 NULL};
676 LOG_IO("gpg", { status = platform_execute_subprocess("gpg", argv_export, NULL, 0); });
677
678 if (status != 0) {
679 platform_unlink(output_path);
680 gpg_homedir_destroy(homedir);
681 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
682 return SET_ERRNO(ERROR_CRYPTO_KEY, "GPG export failed. Check passphrase and key format.");
683 }
684
685 // Read the decrypted output
686 FILE *output_file = platform_fopen("file_stream", output_path, "r");
687 if (!output_file) {
688 platform_unlink(output_path);
689 gpg_homedir_destroy(homedir);
690 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
691 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to read decrypted GPG output");
692 }
693
694 fseek(output_file, 0, SEEK_END);
695 long output_size = ftell(output_file);
696 fseek(output_file, 0, SEEK_SET);
697
698 if (output_size <= 0 || output_size > 1024 * 1024) {
699 fclose(output_file);
700 platform_unlink(output_path);
701 gpg_homedir_destroy(homedir);
702 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
703 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid decrypted GPG output size: %ld bytes", output_size);
704 }
705
706 // Read the decrypted key data
707 *decrypted_out = SAFE_MALLOC((size_t)output_size + 1, char *);
708 if (!*decrypted_out) {
709 fclose(output_file);
710 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for decrypted GPG output");
711 }
712 size_t bytes_read = fread(*decrypted_out, 1, (size_t)output_size, output_file);
713 (*decrypted_out)[bytes_read] = '\0';
714 fclose(output_file);
715
716 // Clean up temporary output file
717 platform_unlink(output_path);
718
719 // Clean up temporary GPG homedir (automatic: deletes entire directory)
720 gpg_homedir_destroy(homedir);
721
722 // Securely erase passphrase from memory
723 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
724
725 log_debug("Successfully decrypted GPG key using passphrase");
726 return ASCIICHAT_OK;
727}
728
729asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32],
730 uint8_t ed25519_sk[32]) {
731 if (!armored_text || !ed25519_pk || !ed25519_sk) {
732 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored seckey parsing");
733 }
734
735 // Find the BEGIN marker (try both "PRIVATE KEY" and "SECRET KEY" formats)
736 const char *begin = strstr(armored_text, "-----BEGIN PGP PRIVATE KEY BLOCK-----");
737 if (!begin) {
738 begin = strstr(armored_text, "-----BEGIN PGP SECRET KEY BLOCK-----");
739 }
740 if (!begin) {
741 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK BEGIN marker");
742 }
743
744 // Skip to the end of the BEGIN line
745 const char *base64_start = strchr(begin, '\n');
746 if (!base64_start) {
747 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
748 }
749 base64_start++; // Skip the newline
750
751 // Find the END marker (try both formats)
752 const char *end = strstr(base64_start, "-----END PGP PRIVATE KEY BLOCK-----");
753 if (!end) {
754 end = strstr(base64_start, "-----END PGP SECRET KEY BLOCK-----");
755 }
756 if (!end) {
757 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK END marker");
758 }
759
760 // Find the checksum line (starts with '=') and exclude it
761 const char *base64_end = end;
762 const char *checksum = base64_end;
763 while (checksum > base64_start && *checksum != '=') {
764 checksum--;
765 }
766 if (*checksum == '=') {
767 // Move back to before the checksum line
768 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
769 checksum--;
770 }
771 base64_end = checksum;
772 }
773
774 size_t base64_len = (size_t)(base64_end - base64_start);
775
776 log_debug("Extracting base64 data from PGP secret key armor (%zu bytes)", base64_len);
777
778 // Decode base64 to binary OpenPGP packets
779 uint8_t *binary_data;
780 size_t binary_len;
781 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
782 if (decode_result != ASCIICHAT_OK) {
783 return decode_result;
784 }
785
786 log_debug("Decoded %zu bytes of OpenPGP secret key packet data", binary_len);
787
788 // Parse OpenPGP packets to find the secret key packet (tag 5)
789 size_t offset = 0;
790 bool found_seckey = false;
791
792 while (offset < binary_len) {
794 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
795 if (header_result != ASCIICHAT_OK) {
796 SAFE_FREE(binary_data);
797 return header_result;
798 }
799
800 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
801
802 // Check if this is a secret key packet (tag 5)
803 if (header.tag == OPENPGP_TAG_SECRET_KEY) {
805 asciichat_error_t parse_result =
806 openpgp_parse_secret_key_packet(binary_data + offset + header.header_len, header.length, &seckey);
807
808 if (parse_result == ASCIICHAT_OK) {
809 // Check if key is encrypted
810 if (seckey.is_encrypted) {
811 SAFE_FREE(binary_data);
812 log_debug("Detected encrypted GPG key, attempting to decrypt with passphrase");
813
814 // Decrypt the key using gpg binary
815 char *decrypted_text = NULL;
816 asciichat_error_t decrypt_result = openpgp_decrypt_with_gpg(armored_text, &decrypted_text);
817 if (decrypt_result != ASCIICHAT_OK) {
818 return decrypt_result;
819 }
820
821 // Recursively parse the decrypted key
822 asciichat_error_t recursive_result = openpgp_parse_armored_seckey(decrypted_text, ed25519_pk, ed25519_sk);
823 SAFE_FREE(decrypted_text);
824 return recursive_result;
825 }
826
827 // Unencrypted key - extract directly
828 memcpy(ed25519_pk, seckey.pubkey, 32);
829 memcpy(ed25519_sk, seckey.seckey, 32);
830 found_seckey = true;
831 log_debug("Extracted Ed25519 keypair from OpenPGP armored secret key block");
832 break;
833 } else {
834 // Not an Ed25519 key, try next packet
835 log_debug("Skipping non-Ed25519 secret key packet");
836 }
837 }
838
839 // Move to next packet
840 offset += header.header_len + header.length;
841 }
842
843 SAFE_FREE(binary_data);
844
845 if (!found_seckey) {
846 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 secret key found in PGP armored block");
847 }
848
849 return ASCIICHAT_OK;
850}
Platform abstraction layer umbrella header providing unified cross-platform API.
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Cross-platform filesystem operations.
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
unsigned char uint8_t
Definition common.h:56
#define OPENPGP_TAG_SECRET_KEY
OpenPGP packet tag for Secret Key Packet.
Definition openpgp.h:44
asciichat_error_t openpgp_extract_ed25519_from_mpi(const uint8_t *mpi, size_t mpi_len, uint8_t ed25519_pk[32])
Extract Ed25519 public key from MPI-encoded data.
Definition openpgp.c:167
asciichat_error_t openpgp_base64_decode(const char *base64, size_t base64_len, uint8_t **binary_out, size_t *binary_len)
Decode PGP armored base64 data.
Definition openpgp.c:32
asciichat_error_t openpgp_parse_armored_pubkey(const char *armored_text, uint8_t ed25519_pk[32])
Parse PGP armored key block and extract Ed25519 public key.
Definition openpgp.c:290
asciichat_error_t openpgp_parse_packet_header(const uint8_t *data, size_t data_len, openpgp_packet_header_t *header)
Parse OpenPGP packet header.
Definition openpgp.c:70
asciichat_error_t openpgp_parse_public_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_public_key_t *pubkey)
Parse OpenPGP Public Key Packet (tag 6)
Definition openpgp.c:200
#define OPENPGP_TAG_PUBLIC_KEY
OpenPGP packet tag for Public Key Packet.
Definition openpgp.h:41
asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
Parse PGP armored secret key block and extract Ed25519 keypair.
Definition openpgp.c:729
asciichat_error_t openpgp_parse_secret_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
Parse OpenPGP Secret Key Packet (tag 5)
Definition openpgp.c:390
#define OPENPGP_ALGO_EDDSA
OpenPGP algorithm ID for EdDSA (Ed25519)
Definition openpgp.h:53
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
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.
int platform_prompt_question(const char *prompt, char *buffer, size_t max_len, prompt_opts_t opts)
Prompt the user for text input.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define PROMPT_OPTS_PASSWORD
Prompt options for password input (no echo, asterisk masking, same line)
Definition question.h:45
bool platform_is_interactive(void)
Check if interactive prompting is available.
int platform_unlink(const char *pathname)
Delete/unlink file.
int platform_open(const char *name, const char *pathname, int flags,...)
Safe file open (open replacement)
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)
int platform_delete_temp_file(const char *path)
Delete a temporary file.
const char * gpg_homedir_path(const gpg_homedir_t *homedir)
Get the homedir path for use in GPG commands.
Definition homedir.c:48
void gpg_homedir_destroy(gpg_homedir_t *homedir)
Destroy a temporary GPG homedir and clean up all files.
Definition homedir.c:55
gpg_homedir_t * gpg_homedir_create(void)
Create a temporary GPG homedir for isolated key operations.
Definition homedir.c:22
Temporary GPG homedir management for isolated key operations.
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
OpenPGP (RFC 4880) packet format parser.
Cross-platform interactive prompting utilities.
Opaque handle to a temporary GPG homedir.
OpenPGP packet header information.
Definition openpgp.h:71
size_t header_len
Header length (bytes consumed by header)
Definition openpgp.h:74
bool new_format
True if new format, false if old format.
Definition openpgp.h:75
uint8_t tag
Packet tag (type identifier)
Definition openpgp.h:72
size_t length
Packet body length.
Definition openpgp.h:73
OpenPGP public key packet data.
Definition openpgp.h:91
uint64_t keyid
OpenPGP Key ID (last 8 bytes of fingerprint)
Definition openpgp.h:96
uint32_t created
Creation timestamp (Unix epoch)
Definition openpgp.h:93
uint8_t pubkey[32]
Ed25519 public key (32 bytes)
Definition openpgp.h:95
uint8_t algorithm
Public key algorithm (22 = EdDSA)
Definition openpgp.h:94
uint8_t version
Packet version (should be 4)
Definition openpgp.h:92
OpenPGP secret key packet data.
Definition openpgp.h:112
uint8_t seckey[32]
Ed25519 secret key (32 bytes)
Definition openpgp.h:117
uint8_t pubkey[32]
Ed25519 public key (32 bytes)
Definition openpgp.h:116
bool is_encrypted
True if secret key material is encrypted.
Definition openpgp.h:119
uint32_t created
Creation timestamp (Unix epoch)
Definition openpgp.h:114
uint8_t algorithm
Public key algorithm (22 = EdDSA)
Definition openpgp.h:115
uint8_t version
Packet version (should be 4)
Definition openpgp.h:113
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69