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>
34 if (!base64 || !binary_out || !binary_len) {
35 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for base64 decode");
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];
47 size_t clean_len = (size_t)(clean_ptr - clean_base64);
50 *binary_out = SAFE_MALLOC(clean_len, uint8_t *);
53 int result = sodium_base642bin(*binary_out, clean_len, clean_base64, clean_len, NULL, binary_len, &end,
54 sodium_base64_VARIANT_ORIGINAL);
56 SAFE_FREE(clean_base64);
59 SAFE_FREE(*binary_out);
60 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to decode base64 PGP armored data");
71 if (!data || !header || data_len == 0) {
72 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for packet header parsing");
75 memset(header, 0,
sizeof(openpgp_packet_header_t));
77 uint8_t ctb = data[0];
80 if ((ctb & 0x80) == 0) {
81 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid OpenPGP packet: bit 7 not set in CTB");
87 header->new_format =
true;
88 header->tag = ctb & 0x3F;
91 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for new format packet header");
94 uint8_t len_byte = data[1];
98 header->length = len_byte;
99 header->header_len = 2;
100 }
else if (len_byte < 224) {
103 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for two-octet length");
105 header->length = ((len_byte - 192) << 8) + data[2] + 192;
106 header->header_len = 3;
107 }
else if (len_byte == 255) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for five-octet length");
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;
116 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Partial body length not supported");
120 header->new_format =
false;
121 header->tag = (ctb >> 2) & 0x0F;
122 uint8_t length_type = ctb & 0x03;
124 switch (length_type) {
127 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for one-octet length");
129 header->length = data[1];
130 header->header_len = 2;
135 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for two-octet length");
137 header->length = ((size_t)data[1] << 8) | data[2];
138 header->header_len = 3;
143 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for four-octet length");
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;
150 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Indeterminate length not supported");
153 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid length type: %u", length_type);
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);
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)");
177 uint16_t bit_count = ((uint16_t)mpi[0] << 8) | mpi[1];
178 log_debug(
"MPI bit count: %u", bit_count);
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);
186 if (mpi[2] != 0x40) {
187 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Missing 0x40 prefix byte in Ed25519 MPI (found 0x%02x)", mpi[2]);
191 memcpy(ed25519_pk, mpi + 3, 32);
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");
206 memset(pubkey, 0,
sizeof(openpgp_public_key_t));
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)",
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];
223 pubkey->algorithm = packet_body[offset++];
225 log_debug(
"Public key packet: version=%u, created=%u, algorithm=%u", pubkey->version, pubkey->created,
229 if (pubkey->algorithm != OPENPGP_ALGO_EDDSA) {
230 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Unsupported public key algorithm: %u (only EdDSA/22 supported)",
242 bool found_prefix =
false;
243 size_t key_offset = 0;
245 for (
size_t i = offset; i < body_len - 32; i++) {
246 if (packet_body[i] == 0x40) {
249 log_debug(
"Found Ed25519 key prefix 0x40 at offset %zu", i);
255 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Ed25519 public key prefix (0x40) not found in packet");
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)");
263 memcpy(pubkey->pubkey, packet_body + key_offset, 32);
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]);
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]);
291 if (!armored_text || !ed25519_pk) {
292 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for armored pubkey parsing");
296 const char *begin = strstr(armored_text,
"-----BEGIN PGP PUBLIC KEY BLOCK-----");
298 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Missing PGP PUBLIC KEY BLOCK BEGIN marker");
302 const char *base64_start = strchr(begin,
'\n');
304 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid PGP armored format: no newline after BEGIN marker");
309 const char *end = strstr(base64_start,
"-----END PGP PUBLIC KEY BLOCK-----");
311 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Missing PGP PUBLIC KEY BLOCK END marker");
315 const char *base64_end = end;
316 const char *checksum = base64_end;
317 while (checksum > base64_start && *checksum !=
'=') {
320 if (*checksum ==
'=') {
322 while (checksum > base64_start && (checksum[-1] ==
'\n' || checksum[-1] ==
'\r')) {
325 base64_end = checksum;
328 size_t base64_len = (size_t)(base64_end - base64_start);
330 log_debug(
"Extracting base64 data from PGP armor (%zu bytes)", base64_len);
333 uint8_t *binary_data;
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;
340 log_debug(
"Decoded %zu bytes of OpenPGP packet data", binary_len);
344 bool found_pubkey =
false;
346 while (offset < binary_len) {
347 openpgp_packet_header_t header;
349 if (header_result != ASCIICHAT_OK) {
350 SAFE_FREE(binary_data);
351 return header_result;
354 log_debug(
"Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
357 if (header.tag == OPENPGP_TAG_PUBLIC_KEY) {
358 openpgp_public_key_t pubkey;
359 asciichat_error_t parse_result =
362 if (parse_result == ASCIICHAT_OK) {
363 memcpy(ed25519_pk, pubkey.pubkey, 32);
365 log_debug(
"Extracted Ed25519 public key from OpenPGP armored block");
369 log_debug(
"Skipping non-Ed25519 public key packet");
374 offset += header.header_len + header.length;
377 SAFE_FREE(binary_data);
380 return SET_ERRNO(ERROR_CRYPTO_KEY,
"No Ed25519 public key found in PGP armored block");
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");
396 memset(seckey, 0,
sizeof(openpgp_secret_key_t));
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)",
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];
414 seckey->algorithm = packet_body[offset++];
416 log_debug(
"Secret key packet: version=%u, created=%u, algorithm=%u", seckey->version, seckey->created,
420 if (seckey->algorithm != OPENPGP_ALGO_EDDSA) {
421 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Unsupported secret key algorithm: %u (only EdDSA/22 supported)",
427 bool found_prefix =
false;
428 size_t pubkey_offset = 0;
430 for (
size_t i = offset; i < body_len - 32; i++) {
431 if (packet_body[i] == 0x40) {
432 pubkey_offset = i + 1;
434 log_debug(
"Found Ed25519 public key prefix 0x40 at offset %zu", i);
440 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Ed25519 public key prefix (0x40) not found in secret key packet");
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)");
448 memcpy(seckey->pubkey, packet_body + pubkey_offset, 32);
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]);
455 offset = pubkey_offset + 32;
460 if (offset >= body_len) {
461 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Missing S2K usage byte in secret key packet");
464 uint8_t s2k_usage = packet_body[offset++];
465 log_debug(
"S2K usage byte: 0x%02x", s2k_usage);
467 if (s2k_usage != 0x00) {
468 seckey->is_encrypted =
true;
469 log_debug(
"Detected encrypted secret key (S2K usage = 0x%02x)", s2k_usage);
474 seckey->is_encrypted =
false;
478 if (offset + 32 > body_len) {
479 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Insufficient data for Ed25519 secret key (need 32 bytes)");
483 memcpy(seckey->seckey, packet_body + offset, 32);
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]);
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");
510 const char *passphrase = SAFE_GETENV(
"ASCII_CHAT_KEY_PASSWORD");
511 char passphrase_buffer[512] = {0};
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");
521 passphrase = passphrase_buffer;
523 return SET_ERRNO(ERROR_CRYPTO_KEY,
524 "Encrypted GPG key requires passphrase. Set ASCII_CHAT_KEY_PASSWORD environment variable or "
525 "run interactively.");
532 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
533 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to create temporary GPG homedir");
539 if (platform_create_temp_file(input_path,
sizeof(input_path),
"asc_gpg_in", &input_fd) != 0) {
541 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
542 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to create temporary input file");
549 platform_delete_temp_file(input_path);
551 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
552 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to open temporary input file");
557 size_t armored_len = strlen(armored_text);
558 ssize_t written = write(input_fd, armored_text, armored_len);
561 if (written != (ssize_t)armored_len) {
562 platform_unlink(input_path);
564 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
565 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to write armored key to temp file");
571 if (platform_create_temp_file(output_path,
sizeof(output_path),
"asc_gpg_out", &output_fd) != 0) {
572 platform_unlink(input_path);
574 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
575 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to create temporary output file");
577 if (output_fd >= 0) {
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);
597 int status = system(command);
600 platform_unlink(input_path);
603 platform_unlink(output_path);
605 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
606 return SET_ERRNO(ERROR_CRYPTO_KEY,
"GPG decryption failed. Check passphrase and key format.");
612 platform_unlink(output_path);
614 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
615 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to read decrypted GPG output");
618 fseek(output_file, 0, SEEK_END);
619 long output_size = ftell(output_file);
620 fseek(output_file, 0, SEEK_SET);
622 if (output_size <= 0 || output_size > 1024 * 1024) {
624 platform_unlink(output_path);
626 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
627 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid decrypted GPG output size: %ld bytes", output_size);
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';
637 platform_unlink(output_path);
643 sodium_memzero(passphrase_buffer,
sizeof(passphrase_buffer));
645 log_debug(
"Successfully decrypted GPG key using passphrase");
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");
656 const char *begin = strstr(armored_text,
"-----BEGIN PGP PRIVATE KEY BLOCK-----");
658 begin = strstr(armored_text,
"-----BEGIN PGP SECRET KEY BLOCK-----");
661 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Missing PGP PRIVATE/SECRET KEY BLOCK BEGIN marker");
665 const char *base64_start = strchr(begin,
'\n');
667 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid PGP armored format: no newline after BEGIN marker");
672 const char *end = strstr(base64_start,
"-----END PGP PRIVATE KEY BLOCK-----");
674 end = strstr(base64_start,
"-----END PGP SECRET KEY BLOCK-----");
677 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Missing PGP PRIVATE/SECRET KEY BLOCK END marker");
681 const char *base64_end = end;
682 const char *checksum = base64_end;
683 while (checksum > base64_start && *checksum !=
'=') {
686 if (*checksum ==
'=') {
688 while (checksum > base64_start && (checksum[-1] ==
'\n' || checksum[-1] ==
'\r')) {
691 base64_end = checksum;
694 size_t base64_len = (size_t)(base64_end - base64_start);
696 log_debug(
"Extracting base64 data from PGP secret key armor (%zu bytes)", base64_len);
699 uint8_t *binary_data;
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;
706 log_debug(
"Decoded %zu bytes of OpenPGP secret key packet data", binary_len);
710 bool found_seckey =
false;
712 while (offset < binary_len) {
713 openpgp_packet_header_t header;
715 if (header_result != ASCIICHAT_OK) {
716 SAFE_FREE(binary_data);
717 return header_result;
720 log_debug(
"Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
723 if (header.tag == OPENPGP_TAG_SECRET_KEY) {
724 openpgp_secret_key_t seckey;
725 asciichat_error_t parse_result =
728 if (parse_result == ASCIICHAT_OK) {
730 if (seckey.is_encrypted) {
731 SAFE_FREE(binary_data);
732 log_debug(
"Detected encrypted GPG key, attempting to decrypt with passphrase");
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;
743 SAFE_FREE(decrypted_text);
744 return recursive_result;
748 memcpy(ed25519_pk, seckey.pubkey, 32);
749 memcpy(ed25519_sk, seckey.seckey, 32);
751 log_debug(
"Extracted Ed25519 keypair from OpenPGP armored secret key block");
755 log_debug(
"Skipping non-Ed25519 secret key packet");
760 offset += header.header_len + header.length;
763 SAFE_FREE(binary_data);
766 return SET_ERRNO(ERROR_CRYPTO_KEY,
"No Ed25519 secret key found in PGP armored block");
const char * gpg_homedir_path(const gpg_homedir_t *homedir)
void gpg_homedir_destroy(gpg_homedir_t *homedir)
gpg_homedir_t * gpg_homedir_create(void)
asciichat_error_t openpgp_extract_ed25519_from_mpi(const uint8_t *mpi, size_t mpi_len, uint8_t ed25519_pk[32])
asciichat_error_t openpgp_base64_decode(const char *base64, size_t base64_len, uint8_t **binary_out, size_t *binary_len)
asciichat_error_t openpgp_parse_armored_pubkey(const char *armored_text, uint8_t ed25519_pk[32])
asciichat_error_t openpgp_parse_packet_header(const uint8_t *data, size_t data_len, openpgp_packet_header_t *header)
asciichat_error_t openpgp_parse_public_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_public_key_t *pubkey)
asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
asciichat_error_t openpgp_parse_secret_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
#define PLATFORM_MAX_PATH_LENGTH
FILE * platform_fopen(const char *filename, const char *mode)
int platform_open(const char *pathname, int flags,...)