ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
openpgp.c
Go to the documentation of this file.
1
7#include <ascii-chat/crypto/gpg/openpgp.h>
8#include <ascii-chat/crypto/gpg/homedir.h>
9#include <ascii-chat/common.h>
10#include <ascii-chat/asciichat_errno.h>
11#include <ascii-chat/log/logging.h>
12#include <ascii-chat/platform/question.h>
13#include <ascii-chat/platform/util.h>
14#include <ascii-chat/platform/abstraction.h>
15#include <ascii-chat/platform/filesystem.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
70asciichat_error_t openpgp_parse_packet_header(const uint8_t *data, size_t data_len, openpgp_packet_header_t *header) {
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
200asciichat_error_t openpgp_parse_public_key_packet(const uint8_t *packet_body, size_t body_len,
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) {
347 openpgp_packet_header_t header;
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) {
358 openpgp_public_key_t pubkey;
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
390asciichat_error_t openpgp_parse_secret_key_packet(const uint8_t *packet_body, size_t body_len,
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
515 if (platform_is_interactive()) {
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 {
523 return SET_ERRNO(ERROR_CRYPTO_KEY,
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)
530 gpg_homedir_t *homedir = gpg_homedir_create();
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(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 // Build GPG command using temporary homedir for isolation
582 // Steps:
583 // 1. Import the key into the temporary homedir (not the user's keyring)
584 // 2. Get the key fingerprint from the temporary homedir
585 // 3. Export the key unencrypted with the provided passphrase
586 const char *homedir_path = gpg_homedir_path(homedir);
587 char command[4096];
589 command, sizeof(command),
590 "gpg --homedir '%s' --batch --import '%s' " PLATFORM_SHELL_NULL_REDIRECT " && "
591 "KEY_FPR=$(gpg --homedir '%s' --list-secret-keys --with-colons " PLATFORM_SHELL_NULL_REDIRECT
592 " | grep '^fpr' | head -1 | cut -d: -f10) && "
593 "gpg --homedir '%s' --batch --pinentry-mode loopback --passphrase '%s' --armor --export-secret-keys "
594 "--export-options export-minimal,no-export-attributes \"$KEY_FPR\" > '%s' " PLATFORM_SHELL_NULL_REDIRECT,
595 homedir_path, input_path, homedir_path, homedir_path, passphrase, output_path);
596
597 int status = system(command);
598
599 // Clean up input file (no longer needed)
600 platform_unlink(input_path);
601
602 if (status != 0) {
603 platform_unlink(output_path);
604 gpg_homedir_destroy(homedir); // Auto-cleanup the temp homedir
605 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
606 return SET_ERRNO(ERROR_CRYPTO_KEY, "GPG decryption failed. Check passphrase and key format.");
607 }
608
609 // Read the decrypted output
610 FILE *output_file = platform_fopen(output_path, "r");
611 if (!output_file) {
612 platform_unlink(output_path);
613 gpg_homedir_destroy(homedir);
614 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
615 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to read decrypted GPG output");
616 }
617
618 fseek(output_file, 0, SEEK_END);
619 long output_size = ftell(output_file);
620 fseek(output_file, 0, SEEK_SET);
621
622 if (output_size <= 0 || output_size > 1024 * 1024) {
623 fclose(output_file);
624 platform_unlink(output_path);
625 gpg_homedir_destroy(homedir);
626 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
627 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid decrypted GPG output size: %ld bytes", output_size);
628 }
629
630 // Read the decrypted key data
631 *decrypted_out = SAFE_MALLOC((size_t)output_size + 1, char *);
632 size_t bytes_read = fread(*decrypted_out, 1, (size_t)output_size, output_file);
633 (*decrypted_out)[bytes_read] = '\0';
634 fclose(output_file);
635
636 // Clean up temporary output file
637 platform_unlink(output_path);
638
639 // Clean up temporary GPG homedir (automatic: deletes entire directory)
640 gpg_homedir_destroy(homedir);
641
642 // Securely erase passphrase from memory
643 sodium_memzero(passphrase_buffer, sizeof(passphrase_buffer));
644
645 log_debug("Successfully decrypted GPG key using passphrase");
646 return ASCIICHAT_OK;
647}
648
649asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32],
650 uint8_t ed25519_sk[32]) {
651 if (!armored_text || !ed25519_pk || !ed25519_sk) {
652 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored seckey parsing");
653 }
654
655 // Find the BEGIN marker (try both "PRIVATE KEY" and "SECRET KEY" formats)
656 const char *begin = strstr(armored_text, "-----BEGIN PGP PRIVATE KEY BLOCK-----");
657 if (!begin) {
658 begin = strstr(armored_text, "-----BEGIN PGP SECRET KEY BLOCK-----");
659 }
660 if (!begin) {
661 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK BEGIN marker");
662 }
663
664 // Skip to the end of the BEGIN line
665 const char *base64_start = strchr(begin, '\n');
666 if (!base64_start) {
667 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
668 }
669 base64_start++; // Skip the newline
670
671 // Find the END marker (try both formats)
672 const char *end = strstr(base64_start, "-----END PGP PRIVATE KEY BLOCK-----");
673 if (!end) {
674 end = strstr(base64_start, "-----END PGP SECRET KEY BLOCK-----");
675 }
676 if (!end) {
677 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK END marker");
678 }
679
680 // Find the checksum line (starts with '=') and exclude it
681 const char *base64_end = end;
682 const char *checksum = base64_end;
683 while (checksum > base64_start && *checksum != '=') {
684 checksum--;
685 }
686 if (*checksum == '=') {
687 // Move back to before the checksum line
688 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
689 checksum--;
690 }
691 base64_end = checksum;
692 }
693
694 size_t base64_len = (size_t)(base64_end - base64_start);
695
696 log_debug("Extracting base64 data from PGP secret key armor (%zu bytes)", base64_len);
697
698 // Decode base64 to binary OpenPGP packets
699 uint8_t *binary_data;
700 size_t binary_len;
701 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
702 if (decode_result != ASCIICHAT_OK) {
703 return decode_result;
704 }
705
706 log_debug("Decoded %zu bytes of OpenPGP secret key packet data", binary_len);
707
708 // Parse OpenPGP packets to find the secret key packet (tag 5)
709 size_t offset = 0;
710 bool found_seckey = false;
711
712 while (offset < binary_len) {
713 openpgp_packet_header_t header;
714 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
715 if (header_result != ASCIICHAT_OK) {
716 SAFE_FREE(binary_data);
717 return header_result;
718 }
719
720 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
721
722 // Check if this is a secret key packet (tag 5)
723 if (header.tag == OPENPGP_TAG_SECRET_KEY) {
724 openpgp_secret_key_t seckey;
725 asciichat_error_t parse_result =
726 openpgp_parse_secret_key_packet(binary_data + offset + header.header_len, header.length, &seckey);
727
728 if (parse_result == ASCIICHAT_OK) {
729 // Check if key is encrypted
730 if (seckey.is_encrypted) {
731 SAFE_FREE(binary_data);
732 log_debug("Detected encrypted GPG key, attempting to decrypt with passphrase");
733
734 // Decrypt the key using gpg binary
735 char *decrypted_text = NULL;
736 asciichat_error_t decrypt_result = openpgp_decrypt_with_gpg(armored_text, &decrypted_text);
737 if (decrypt_result != ASCIICHAT_OK) {
738 return decrypt_result;
739 }
740
741 // Recursively parse the decrypted key
742 asciichat_error_t recursive_result = openpgp_parse_armored_seckey(decrypted_text, ed25519_pk, ed25519_sk);
743 SAFE_FREE(decrypted_text);
744 return recursive_result;
745 }
746
747 // Unencrypted key - extract directly
748 memcpy(ed25519_pk, seckey.pubkey, 32);
749 memcpy(ed25519_sk, seckey.seckey, 32);
750 found_seckey = true;
751 log_debug("Extracted Ed25519 keypair from OpenPGP armored secret key block");
752 break;
753 } else {
754 // Not an Ed25519 key, try next packet
755 log_debug("Skipping non-Ed25519 secret key packet");
756 }
757 }
758
759 // Move to next packet
760 offset += header.header_len + header.length;
761 }
762
763 SAFE_FREE(binary_data);
764
765 if (!found_seckey) {
766 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 secret key found in PGP armored block");
767 }
768
769 return ASCIICHAT_OK;
770}
const char * gpg_homedir_path(const gpg_homedir_t *homedir)
Definition homedir.c:49
void gpg_homedir_destroy(gpg_homedir_t *homedir)
Definition homedir.c:56
gpg_homedir_t * gpg_homedir_create(void)
Definition homedir.c:23
asciichat_error_t openpgp_extract_ed25519_from_mpi(const uint8_t *mpi, size_t mpi_len, uint8_t ed25519_pk[32])
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)
Definition openpgp.c:32
asciichat_error_t openpgp_parse_armored_pubkey(const char *armored_text, uint8_t ed25519_pk[32])
Definition openpgp.c:290
asciichat_error_t openpgp_parse_packet_header(const uint8_t *data, size_t data_len, openpgp_packet_header_t *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)
Definition openpgp.c:200
asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
Definition openpgp.c:649
asciichat_error_t openpgp_parse_secret_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
Definition openpgp.c:390
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
FILE * platform_fopen(const char *filename, const char *mode)
int platform_open(const char *pathname, int flags,...)