30 if (!input || !key_out) {
31 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for public key parsing");
35 memset(key_out, 0,
sizeof(public_key_t));
38 if (strncmp(input,
"ssh-ed25519", 11) == 0) {
39 key_out->type = KEY_TYPE_ED25519;
41 if (result == ASCIICHAT_OK) {
42 platform_strncpy(key_out->comment,
sizeof(key_out->comment),
"ssh-ed25519",
sizeof(key_out->comment) - 1);
49 size_t input_len = strlen(input);
50 if (input_len >= 50 && input_len <= 80) {
52 bool looks_like_base64 =
true;
53 for (
size_t i = 0; i < input_len; i++) {
55 if (!((c >=
'A' && c <=
'Z') || (c >=
'a' && c <=
'z') || (c >=
'0' && c <=
'9') || c ==
'+' || c ==
'/' ||
57 looks_like_base64 =
false;
62 if (looks_like_base64) {
64 uint8_t *blob = SAFE_MALLOC(input_len, uint8_t *);
67 const char *end = NULL;
68 int decode_result = sodium_base642bin(blob, input_len, input, input_len,
70 &blob_len, &end, sodium_base64_VARIANT_ORIGINAL);
72 if (decode_result == 0 && blob_len >= 47) {
76 uint32_t key_type_len =
77 ((uint32_t)blob[0] << 24) | ((uint32_t)blob[1] << 16) | ((uint32_t)blob[2] << 8) | blob[3];
78 if (key_type_len == 11 && blob_len >= 15 && memcmp(blob + 4,
"ssh-ed25519", 11) == 0) {
82 ((uint32_t)blob[15] << 24) | ((uint32_t)blob[16] << 16) | ((uint32_t)blob[17] << 8) | blob[18];
83 if (pubkey_len == 32 && blob_len >= 51) {
85 key_out->type = KEY_TYPE_ED25519;
86 memcpy(key_out->key, blob + 19, 32);
87 platform_strncpy(key_out->comment,
sizeof(key_out->comment),
"raw-base64",
88 sizeof(key_out->comment) - 1);
101 if (strncmp(input,
"gpg:", 4) == 0) {
108 url_parts_t url_parts = {0};
109 asciichat_error_t parse_result =
url_parse(input, &url_parts);
110 if (parse_result != ASCIICHAT_OK) {
111 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to parse HTTPS URL: %s", input);
115 char *response =
https_get(url_parts.host, url_parts.path);
119 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to fetch key from HTTPS URL: %s", input);
129 if (strncmp(input,
"github:", 7) == 0 || strncmp(input,
"gitlab:", 7) == 0) {
130 public_key_t keys[1];
133 if (result == ASCIICHAT_OK && num_keys > 0) {
140 if (strlen(input) == 64) {
142 bool is_valid_hex =
true;
143 for (
int i = 0; i < 64; i++) {
144 if (!((input[i] >=
'0' && input[i] <=
'9') || (input[i] >=
'a' && input[i] <=
'f') ||
145 (input[i] >=
'A' && input[i] <=
'F'))) {
146 is_valid_hex =
false;
153 key_out->type = KEY_TYPE_X25519;
155 if (
hex_decode(input, key_out->key, 32) != ASCIICHAT_OK) {
156 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to decode hex key");
158 platform_strncpy(key_out->comment,
sizeof(key_out->comment),
"raw-hex",
sizeof(key_out->comment) - 1);
164 if (strstr(input,
"-----BEGIN PGP PUBLIC KEY BLOCK-----") != NULL) {
169 char *normalized_path = NULL;
171 if (path_result != ASCIICHAT_OK) {
172 SAFE_FREE(normalized_path);
178 char line[BUFFER_SIZE_LARGE];
179 if (fgets(line,
sizeof(line), f)) {
181 SAFE_FREE(normalized_path);
183 line[strcspn(line,
"\r\n")] = 0;
188 SAFE_FREE(normalized_path);
191 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Unsupported key format: %s", input);
195 if (!key_path || !key_out) {
196 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for private key parsing");
200 memset(key_out, 0,
sizeof(private_key_t));
206 if (strncmp(key_path,
"gpg:", 4) == 0) {
207 const char *key_id = key_path + 4;
210 size_t key_id_len = strlen(key_id);
211 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
212 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)",
217 for (
size_t i = 0; i < key_id_len; i++) {
219 if (!((c >=
'0' && c <=
'9') || (c >=
'a' && c <=
'f') || (c >=
'A' && c <=
'F'))) {
220 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid GPG key ID: contains non-hex character '%c'", c);
225 uint8_t public_key[32];
228 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to get public key from GPG for key ID: %s", key_id);
232 key_out->type = KEY_TYPE_ED25519;
233 key_out->use_gpg_agent =
true;
234 key_out->use_ssh_agent =
false;
237 memcpy(key_out->public_key, public_key, 32);
240 platform_strncpy(key_out->gpg_keygrip,
sizeof(key_out->gpg_keygrip), keygrip,
sizeof(key_out->gpg_keygrip) - 1);
243 safe_snprintf(key_out->key_comment,
sizeof(key_out->key_comment),
"GPG key %s", key_id);
247 memset(key_out->key.ed25519, 0, 64);
251 memcpy(key_out->key.ed25519 + 32, public_key, 32);
253 log_debug(
"Loaded GPG key %s (keygrip: %.40s) for agent signing", key_id, keygrip);
258 char *normalized_path = NULL;
260 if (path_result != ASCIICHAT_OK) {
261 SAFE_FREE(normalized_path);
268 SAFE_FREE(normalized_path);
269 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to open private key file: %s", key_path);
274 size_t bytes_read = fread(header, 1,
sizeof(header) - 1, f);
275 header[bytes_read] =
'\0';
278 log_debug(
"parse_private_key: Read %zu bytes from %s, header: %.50s", bytes_read, key_path, header);
281 if (strstr(header,
"-----BEGIN PGP PRIVATE KEY") != NULL || strstr(header,
"-----BEGIN PGP SECRET KEY") != NULL) {
282 log_debug(
"Detected PGP armored secret key format in %s", key_path);
286 SAFE_FREE(normalized_path);
287 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to open GPG key file: %s", key_path);
291 fseek(f, 0, SEEK_END);
292 long file_size = ftell(f);
293 fseek(f, 0, SEEK_SET);
295 if (file_size <= 0 || file_size > 1024 * 1024) {
297 SAFE_FREE(normalized_path);
298 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid GPG key file size: %ld bytes", file_size);
301 char *file_content = SAFE_MALLOC((
size_t)file_size + 1,
char *);
302 size_t content_read = fread(file_content, 1, (
size_t)file_size, f);
303 file_content[content_read] =
'\0';
307 uint8_t ed25519_pk[32];
308 uint8_t ed25519_sk[32];
310 SAFE_FREE(file_content);
311 SAFE_FREE(normalized_path);
313 if (openpgp_result != ASCIICHAT_OK) {
314 return openpgp_result;
318 key_out->type = KEY_TYPE_ED25519;
319 key_out->use_gpg_agent =
false;
320 key_out->use_ssh_agent =
false;
324 memcpy(key_out->key.ed25519, ed25519_sk, 32);
325 memcpy(key_out->key.ed25519 + 32, ed25519_pk, 32);
326 memcpy(key_out->public_key, ed25519_pk, 32);
329 safe_snprintf(key_out->key_comment,
sizeof(key_out->key_comment),
"GPG Ed25519 key from %s", key_path);
331 log_debug(
"Loaded unencrypted GPG Ed25519 key from %s", key_path);
337 SAFE_FREE(normalized_path);
345asciichat_error_t
parse_public_keys(
const char *input, public_key_t *keys_out,
size_t *num_keys,
size_t max_keys) {
346 if (!input || !keys_out || !num_keys || max_keys == 0) {
347 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for multi-key parsing");
353 if (strchr(input,
',') != NULL) {
354 size_t input_len = strlen(input);
355 char *input_copy = SAFE_MALLOC(input_len + 1,
char *);
356 memcpy(input_copy, input, input_len + 1);
358 char *saveptr = NULL;
359 char *specifier = platform_strtok_r(input_copy,
",", &saveptr);
361 while (specifier != NULL && *num_keys < max_keys) {
363 while (*specifier && isspace((
unsigned char)*specifier)) {
366 char *end = specifier + strlen(specifier) - 1;
367 while (end > specifier && isspace((
unsigned char)*end)) {
372 if (strlen(specifier) > 0) {
374 size_t current_num = 0;
375 asciichat_error_t result =
376 parse_public_keys(specifier, &keys_out[*num_keys], ¤t_num, max_keys - *num_keys);
377 if (result == ASCIICHAT_OK && current_num > 0) {
378 *num_keys += current_num;
380 log_warn(
"Failed to parse key specifier: %s", specifier);
385 specifier = platform_strtok_r(NULL,
",", &saveptr);
388 SAFE_FREE(input_copy);
390 if (*num_keys == 0) {
391 return SET_ERRNO(ERROR_CRYPTO_KEY,
"No valid keys found in comma-separated list: %s", input);
398 if (strncmp(input,
"ssh-ed25519", 11) == 0) {
400 if (result == ASCIICHAT_OK) {
407 if (strncmp(input,
"github:", 7) == 0 || strncmp(input,
"gitlab:", 7) == 0) {
408 const char *username = input + 7;
409 bool is_github = (strncmp(input,
"github:", 7) == 0);
410 bool is_gpg = (strstr(username,
".gpg") != NULL);
413 size_t num_fetched_keys = 0;
414 asciichat_error_t result;
430 if (result != ASCIICHAT_OK || num_fetched_keys == 0) {
431 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to fetch keys from %s for user: %s", is_github ?
"GitHub" :
"GitLab",
436 for (
size_t i = 0; i < num_fetched_keys && *num_keys < max_keys; i++) {
444 for (
size_t i = 0; i < num_fetched_keys; i++) {
449 if (*num_keys == 0) {
450 return SET_ERRNO(ERROR_CRYPTO_KEY,
"No valid Ed25519 keys found for %s user: %s", is_github ?
"GitHub" :
"GitLab",
454 log_debug(
"Parsed %zu Ed25519 key(s) from %s user: %s", *num_keys, is_github ?
"GitHub" :
"GitLab", username);
465 if (result == ASCIICHAT_OK) {
495 if (!key || !x25519_sk) {
496 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for private_key_to_x25519");
499 if (key->type == KEY_TYPE_X25519) {
501 memcpy(x25519_sk, key->key.x25519, 32);
505 if (key->type == KEY_TYPE_ED25519) {
510 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Unsupported key type for X25519 conversion: %d", key->type);
545asciichat_error_t
parse_keys_from_file(
const char *path, public_key_t *keys,
size_t *num_keys,
size_t max_keys) {
546 if (!path || !keys || !num_keys) {
547 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for key file parsing");
553 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Invalid keys file path: %s", path);
556 char *normalized_path = NULL;
558 if (path_result != ASCIICHAT_OK) {
559 SAFE_FREE(normalized_path);
565 SAFE_FREE(normalized_path);
566 return SET_ERRNO(ERROR_CRYPTO_KEY,
"Failed to open keys file: %s", path);
569 char line[BUFFER_SIZE_LARGE];
570 while (fgets(line,
sizeof(line), f) && *num_keys < max_keys) {
572 line[strcspn(line,
"\r\n")] = 0;
575 if (strlen(line) == 0 || line[0] ==
'#') {
585 SAFE_FREE(normalized_path);
627asciichat_error_t
hex_decode(
const char *hex, uint8_t *output,
size_t output_len) {
628 if (!hex || !output) {
629 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for hex_decode");
632 size_t hex_len = strlen(hex);
633 size_t expected_hex_len = output_len * 2;
635 if (hex_len != expected_hex_len) {
636 return SET_ERRNO(ERROR_INVALID_PARAM,
637 "Hex string length (%zu) doesn't match expected output length (%zu * 2 = %zu)", hex_len,
638 output_len, expected_hex_len);
642 for (
size_t i = 0; i < output_len; i++) {
643 char hex_byte[3] = {hex[i * 2], hex[i * 2 + 1],
'\0'};
645 unsigned long byte = strtoul(hex_byte, &endptr, 16);
648 if (*endptr !=
'\0' ||
byte > 255) {
649 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid hex character at position %zu: '%c%c'", i * 2, hex[i * 2],
653 output[i] = (uint8_t)
byte;