ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
keys.c
Go to the documentation of this file.
1
13#include <ascii-chat/common.h>
16#include <ascii-chat/util/url.h> // For url_is_valid(), url_parse()
17#include <ascii-chat/crypto/gpg/export.h> // For gpg_get_public_key()
18#include <ascii-chat/network/http_client.h> // For https_get()
19#include <string.h>
20#include <stdlib.h>
21#include <stdio.h>
22#include <ctype.h>
23
24// =============================================================================
25// High-Level Key Parsing Functions
26// =============================================================================
27
28asciichat_error_t parse_public_key(const char *input, public_key_t *key_out) {
29 if (!input || !key_out) {
30 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key parsing");
31 }
32
33 // Clear output structure
34 memset(key_out, 0, sizeof(public_key_t));
35
36 // Try SSH key parsing first (full format with prefix)
37 if (strncmp(input, "ssh-ed25519", 11) == 0) {
38 key_out->type = KEY_TYPE_ED25519;
39 asciichat_error_t result = parse_ssh_ed25519_line(input, key_out->key);
40 if (result == ASCIICHAT_OK) {
41 platform_strncpy(key_out->comment, sizeof(key_out->comment), "ssh-ed25519", sizeof(key_out->comment) - 1);
42 }
43 return result;
44 }
45
46 // Try raw base64 SSH public key (without "ssh-ed25519" prefix)
47 // SSH Ed25519 public keys in base64 are typically ~68 characters
48 size_t input_len = strlen(input);
49 if (input_len >= 50 && input_len <= 80) {
50 // Check if it looks like base64 (only contains valid base64 chars)
51 bool looks_like_base64 = true;
52 for (size_t i = 0; i < input_len; i++) {
53 char c = input[i];
54 if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '/' ||
55 c == '=')) {
56 looks_like_base64 = false;
57 break;
58 }
59 }
60
61 if (looks_like_base64) {
62 // Try to decode as SSH public key blob
63 uint8_t *blob = SAFE_MALLOC(input_len, uint8_t *);
64 size_t blob_len = 0;
65
66 const char *end = NULL;
67 int decode_result = sodium_base642bin(blob, input_len, input, input_len,
68 NULL, // ignore chars
69 &blob_len, &end, sodium_base64_VARIANT_ORIGINAL);
70
71 if (decode_result == 0 && blob_len >= 47) { // Minimum size: 4+11+4+32 = 51 bytes
72 // Validate SSH key blob structure
73 // Format: [4 bytes: length][11 bytes: "ssh-ed25519"][4 bytes: length][32 bytes: key]
74 if (blob_len >= 4) {
75 uint32_t key_type_len =
76 ((uint32_t)blob[0] << 24) | ((uint32_t)blob[1] << 16) | ((uint32_t)blob[2] << 8) | blob[3];
77 if (key_type_len == 11 && blob_len >= 15 && memcmp(blob + 4, "ssh-ed25519", 11) == 0) {
78 // Valid ssh-ed25519 blob, extract the 32-byte public key
79 if (blob_len >= 19) {
80 uint32_t pubkey_len =
81 ((uint32_t)blob[15] << 24) | ((uint32_t)blob[16] << 16) | ((uint32_t)blob[17] << 8) | blob[18];
82 if (pubkey_len == 32 && blob_len >= 51) {
83 // Extract Ed25519 public key
84 key_out->type = KEY_TYPE_ED25519;
85 memcpy(key_out->key, blob + 19, 32);
86 platform_strncpy(key_out->comment, sizeof(key_out->comment), "raw-base64",
87 sizeof(key_out->comment) - 1);
88 SAFE_FREE(blob);
89 return ASCIICHAT_OK;
90 }
91 }
92 }
93 }
94 }
95 SAFE_FREE(blob);
96 }
97 }
98
99 // Try GPG key parsing
100 if (strncmp(input, "gpg:", 4) == 0) {
101 return parse_gpg_key(input + 4, key_out);
102 }
103
104 // Try HTTPS URLs (validated via production-grade URL regex)
105 if (url_is_valid(input)) {
106 // Parse HTTPS URL to extract hostname and path
107 url_parts_t url_parts = {0};
108 asciichat_error_t parse_result = url_parse(input, &url_parts);
109 if (parse_result != ASCIICHAT_OK) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to parse HTTPS URL: %s", input);
111 }
112
113 // Fetch the key via HTTPS
114 char *response = https_get(url_parts.host, url_parts.path);
115 url_parts_destroy(&url_parts);
116
117 if (!response) {
118 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch key from HTTPS URL: %s", input);
119 }
120
121 // Parse the fetched content as a public key
122 asciichat_error_t result = parse_public_key(response, key_out);
123 SAFE_FREE(response);
124 return result;
125 }
126
127 // Try HTTPS key fetching (GitHub/GitLab) - delegate to parse_public_keys and return first
128 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
129 public_key_t keys[1];
130 size_t num_keys = 0;
131 asciichat_error_t result = parse_public_keys(input, keys, &num_keys, 1);
132 if (result == ASCIICHAT_OK && num_keys > 0) {
133 *key_out = keys[0];
134 }
135 return result;
136 }
137
138 // Try raw hex key (64 hex chars = 32 bytes)
139 if (strlen(input) == 64) {
140 // Check if it's valid hex
141 bool is_valid_hex = true;
142 for (int i = 0; i < 64; i++) {
143 if (!((input[i] >= '0' && input[i] <= '9') || (input[i] >= 'a' && input[i] <= 'f') ||
144 (input[i] >= 'A' && input[i] <= 'F'))) {
145 is_valid_hex = false;
146 break;
147 }
148 }
149
150 if (is_valid_hex) {
151 // Assume it's a raw X25519 public key in hex (default for raw hex)
152 key_out->type = KEY_TYPE_X25519;
153 // Use hex_decode utility to safely decode hex string to binary
154 if (hex_decode(input, key_out->key, 32) != ASCIICHAT_OK) {
155 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode hex key");
156 }
157 platform_strncpy(key_out->comment, sizeof(key_out->comment), "raw-hex", sizeof(key_out->comment) - 1);
158 return ASCIICHAT_OK;
159 }
160 }
161
162 // Try PGP armored format (-----BEGIN PGP PUBLIC KEY BLOCK-----)
163 if (strstr(input, "-----BEGIN PGP PUBLIC KEY BLOCK-----") != NULL) {
164 return parse_gpg_key_binary((const uint8_t *)input, strlen(input), key_out);
165 }
166
167 if (path_looks_like_path(input)) {
168 char *normalized_path = NULL;
169 asciichat_error_t path_result = path_validate_user_path(input, PATH_ROLE_KEY_PUBLIC, &normalized_path);
170 if (path_result != ASCIICHAT_OK) {
171 SAFE_FREE(normalized_path);
172 return path_result;
173 }
174
175 FILE *f = platform_fopen("file_stream", normalized_path, "r");
176 if (f) {
177 char line[BUFFER_SIZE_LARGE];
178 if (fgets(line, sizeof(line), f)) {
179 (void)fclose(f);
180 SAFE_FREE(normalized_path);
181 // Remove newline
182 line[strcspn(line, "\r\n")] = 0;
183 return parse_public_key(line, key_out);
184 }
185 (void)fclose(f);
186 }
187 SAFE_FREE(normalized_path);
188 }
189
190 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key format: %s", input);
191}
192
193asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out) {
194 if (!key_path || !key_out) {
195 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private key parsing");
196 }
197
198 // Clear output structure
199 memset(key_out, 0, sizeof(private_key_t));
200
201 // Check for GPG key format: "gpg:KEYID" where KEYID is 8, 16, or 40 hex chars
202 // - 8 chars: short key ID (last 8 chars of fingerprint)
203 // - 16 chars: long key ID (last 16 chars of fingerprint)
204 // - 40 chars: full fingerprint
205 if (strncmp(key_path, "gpg:", 4) == 0) {
206 const char *key_id = key_path + 4; // Skip "gpg:" prefix
207
208 // Validate key ID format (must be 8, 16, or 40 hex characters)
209 size_t key_id_len = strlen(key_id);
210 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
211 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)",
212 key_id_len);
213 }
214
215 // Validate hex characters
216 for (size_t i = 0; i < key_id_len; i++) {
217 char c = key_id[i];
218 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
219 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID: contains non-hex character '%c'", c);
220 }
221 }
222
223 // Get public key and keygrip from GPG keyring
224 uint8_t public_key[32];
225 char keygrip[64];
226 if (gpg_get_public_key(key_id, public_key, keygrip) != 0) {
227 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to get public key from GPG for key ID: %s", key_id);
228 }
229
230 // Populate private_key_t structure for GPG agent use
231 key_out->type = KEY_TYPE_ED25519;
232 key_out->use_gpg_agent = true;
233 key_out->use_ssh_agent = false;
234
235 // Store public key (for handshake and verification)
236 memcpy(key_out->public_key, public_key, 32);
237
238 // Store keygrip (for GPG agent signing operations)
239 platform_strncpy(key_out->gpg_keygrip, sizeof(key_out->gpg_keygrip), keygrip, sizeof(key_out->gpg_keygrip) - 1);
240
241 // Store comment for display
242 safe_snprintf(key_out->key_comment, sizeof(key_out->key_comment), "GPG key %s", key_id);
243
244 // Clear the key.ed25519 field (we don't have the private key in memory)
245 // The private key stays protected in GPG agent
246 memset(key_out->key.ed25519, 0, 64);
247
248 // Set the public key in the second half of ed25519 key (standard Ed25519 format)
249 // This is for compatibility with code that expects the public key at offset 32
250 memcpy(key_out->key.ed25519 + 32, public_key, 32);
251
252 log_debug("Loaded GPG key %s (keygrip: %.40s) for agent signing", key_id, keygrip);
253 return ASCIICHAT_OK;
254 }
255
256 // Try to load file and detect format
257 char *normalized_path = NULL;
258 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
259 if (path_result != ASCIICHAT_OK) {
260 SAFE_FREE(normalized_path);
261 return path_result;
262 }
263
264 // Read file content to detect format
265 FILE *f = platform_fopen("file_stream", normalized_path, "r");
266 if (!f) {
267 SAFE_FREE(normalized_path);
268 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open private key file: %s", key_path);
269 }
270
271 // Read first 50 bytes to detect format
272 char header[50];
273 size_t bytes_read = fread(header, 1, sizeof(header) - 1, f);
274 header[bytes_read] = '\0';
275 fclose(f);
276
277 log_debug("parse_private_key: Read %zu bytes from %s, header: %.50s", bytes_read, key_path, header);
278
279 // Check for PGP armored secret key format
280 if (strstr(header, "-----BEGIN PGP PRIVATE KEY") != NULL || strstr(header, "-----BEGIN PGP SECRET KEY") != NULL) {
281 log_debug("Detected PGP armored secret key format in %s", key_path);
282 // Load entire file for OpenPGP parsing
283 f = platform_fopen("file_stream", normalized_path, "r");
284 if (!f) {
285 SAFE_FREE(normalized_path);
286 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open GPG key file: %s", key_path);
287 }
288
289 // Get file size
290 fseek(f, 0, SEEK_END);
291 long file_size = ftell(f);
292 fseek(f, 0, SEEK_SET);
293
294 if (file_size <= 0 || file_size > 1024 * 1024) { // Sanity check: max 1MB
295 fclose(f);
296 SAFE_FREE(normalized_path);
297 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key file size: %ld bytes", file_size);
298 }
299
300 char *file_content = SAFE_MALLOC((size_t)file_size + 1, char *);
301 size_t content_read = fread(file_content, 1, (size_t)file_size, f);
302 file_content[content_read] = '\0';
303 fclose(f);
304
305 // Parse OpenPGP armored secret key
306 uint8_t ed25519_pk[32];
307 uint8_t ed25519_sk[32];
308 asciichat_error_t openpgp_result = openpgp_parse_armored_seckey(file_content, ed25519_pk, ed25519_sk);
309 SAFE_FREE(file_content);
310 SAFE_FREE(normalized_path);
311
312 if (openpgp_result != ASCIICHAT_OK) {
313 return openpgp_result;
314 }
315
316 // Populate private_key_t structure
317 key_out->type = KEY_TYPE_ED25519;
318 key_out->use_gpg_agent = false;
319 key_out->use_ssh_agent = false;
320
321 // Store keypair in Ed25519 format (64 bytes: 32-byte seed + 32-byte public key)
322 // libsodium expects the seed in first 32 bytes and public key in second 32 bytes
323 memcpy(key_out->key.ed25519, ed25519_sk, 32); // Secret key (seed)
324 memcpy(key_out->key.ed25519 + 32, ed25519_pk, 32); // Public key
325 memcpy(key_out->public_key, ed25519_pk, 32); // Also store in public_key field
326
327 // Store comment
328 safe_snprintf(key_out->key_comment, sizeof(key_out->key_comment), "GPG Ed25519 key from %s", key_path);
329
330 log_debug("Loaded unencrypted GPG Ed25519 key from %s", key_path);
331 return ASCIICHAT_OK;
332 }
333
334 // Fall back to SSH private key parsing
335 asciichat_error_t result = parse_ssh_private_key(normalized_path, key_out);
336 SAFE_FREE(normalized_path);
337 return result;
338}
339
340// =============================================================================
341// Multi-Key Public Key Parsing
342// =============================================================================
343
344asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys) {
345 if (!input || !keys_out || !num_keys || max_keys == 0) {
346 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for multi-key parsing");
347 }
348
349 *num_keys = 0;
350
351 // Handle comma-separated key specifiers
352 if (strchr(input, ',') != NULL) {
353 size_t input_len = strlen(input);
354 char *input_copy = SAFE_MALLOC(input_len + 1, char *);
355 memcpy(input_copy, input, input_len + 1);
356
357 char *saveptr = NULL;
358 char *specifier = platform_strtok_r(input_copy, ",", &saveptr);
359
360 while (specifier != NULL && *num_keys < max_keys) {
361 // Trim leading/trailing whitespace
362 while (*specifier && isspace((unsigned char)*specifier)) {
363 specifier++;
364 }
365 char *end = specifier + strlen(specifier) - 1;
366 while (end > specifier && isspace((unsigned char)*end)) {
367 *end-- = '\0';
368 }
369
370 // Skip empty specifiers
371 if (strlen(specifier) > 0) {
372 // Recursively parse this single specifier (will not re-enter comma handling)
373 size_t current_num = 0;
374 asciichat_error_t result =
375 parse_public_keys(specifier, &keys_out[*num_keys], &current_num, max_keys - *num_keys);
376 if (result == ASCIICHAT_OK && current_num > 0) {
377 *num_keys += current_num;
378 } else {
379 log_warn("Failed to parse key specifier: %s", specifier);
380 // Continue parsing remaining specifiers even if one fails
381 }
382 }
383
384 specifier = platform_strtok_r(NULL, ",", &saveptr);
385 }
386
387 SAFE_FREE(input_copy);
388
389 if (*num_keys == 0) {
390 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid keys found in comma-separated list: %s", input);
391 }
392 return ASCIICHAT_OK;
393 }
394
395 // Check for direct SSH Ed25519 key format BEFORE checking for file paths
396 // (SSH keys can contain '/' in base64, which would match path_looks_like_path)
397 if (strncmp(input, "ssh-ed25519", 11) == 0) {
398 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
399 if (result == ASCIICHAT_OK) {
400 *num_keys = 1;
401 }
402 return result;
403 }
404
405 // Check if this is a GitHub/GitLab reference - these support multiple keys
406 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
407 const char *username = input + 7; // Skip "github:" or "gitlab:"
408 bool is_github = (strncmp(input, "github:", 7) == 0);
409 bool is_gpg = (strstr(username, ".gpg") != NULL);
410
411 char **keys = NULL;
412 size_t num_fetched_keys = 0;
413 asciichat_error_t result;
414
415 if (is_github) {
416 if (is_gpg) {
417 result = fetch_github_gpg_keys(username, &keys, &num_fetched_keys);
418 } else {
419 result = fetch_github_ssh_keys(username, &keys, &num_fetched_keys);
420 }
421 } else {
422 if (is_gpg) {
423 result = fetch_gitlab_gpg_keys(username, &keys, &num_fetched_keys);
424 } else {
425 result = fetch_gitlab_ssh_keys(username, &keys, &num_fetched_keys);
426 }
427 }
428
429 if (result != ASCIICHAT_OK || num_fetched_keys == 0) {
430 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch keys from %s for user: %s", is_github ? "GitHub" : "GitLab",
431 username);
432 }
433
434 // Parse each fetched key (only Ed25519 keys will succeed)
435 for (size_t i = 0; i < num_fetched_keys && *num_keys < max_keys; i++) {
436 if (parse_public_key(keys[i], &keys_out[*num_keys]) == ASCIICHAT_OK) {
437 (*num_keys)++;
438 }
439 // Non-Ed25519 keys are silently skipped
440 }
441
442 // Free the keys array
443 for (size_t i = 0; i < num_fetched_keys; i++) {
444 SAFE_FREE(keys[i]);
445 }
446 SAFE_FREE(keys);
447
448 if (*num_keys == 0) {
449 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid Ed25519 keys found for %s user: %s", is_github ? "GitHub" : "GitLab",
450 username);
451 }
452
453 log_debug("Parsed %zu Ed25519 key(s) from %s user: %s", *num_keys, is_github ? "GitHub" : "GitLab", username);
454 return ASCIICHAT_OK;
455 }
456
457 // For file paths, delegate to parse_keys_from_file
458 if (path_looks_like_path(input)) {
459 return parse_keys_from_file(input, keys_out, num_keys, max_keys);
460 }
461
462 // For all other formats, use single-key parsing
463 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
464 if (result == ASCIICHAT_OK) {
465 *num_keys = 1;
466 }
467 return result;
468}
469
470// =============================================================================
471// Key Conversion Functions
472// =============================================================================
473
475 if (!key || !x25519_pk) {
476 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public_key_to_x25519");
477 }
478
479 if (key->type == KEY_TYPE_X25519) {
480 // Passthrough for X25519 keys
481 memcpy(x25519_pk, key->key, 32);
482 return ASCIICHAT_OK;
483 }
484
485 if (key->type == KEY_TYPE_ED25519) {
486 // Convert Ed25519 to X25519
487 return ed25519_to_x25519_public(key->key, x25519_pk);
488 }
489
490 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
491}
492
494 if (!key || !x25519_sk) {
495 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private_key_to_x25519");
496 }
497
498 if (key->type == KEY_TYPE_X25519) {
499 // Passthrough for X25519 keys
500 memcpy(x25519_sk, key->key.x25519, 32);
501 return ASCIICHAT_OK;
502 }
503
504 if (key->type == KEY_TYPE_ED25519) {
505 // Convert Ed25519 to X25519 (Ed25519 private key is 64 bytes: seed + public)
506 return ed25519_to_x25519_private(key->key.ed25519, x25519_sk);
507 }
508
509 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
510}
511
512// =============================================================================
513// HTTPS Key Fetching Wrapper Functions
514// =============================================================================
515
516asciichat_error_t fetch_github_keys(const char *username, char ***keys_out, size_t *num_keys, bool use_gpg) {
517 if (!username || !keys_out || !num_keys) {
518 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_github_keys");
519 }
520
521 if (use_gpg) {
522 return fetch_github_gpg_keys(username, keys_out, num_keys);
523 } else {
524 return fetch_github_ssh_keys(username, keys_out, num_keys);
525 }
526}
527
528asciichat_error_t fetch_gitlab_keys(const char *username, char ***keys_out, size_t *num_keys, bool use_gpg) {
529 if (!username || !keys_out || !num_keys) {
530 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_gitlab_keys");
531 }
532
533 if (use_gpg) {
534 return fetch_gitlab_gpg_keys(username, keys_out, num_keys);
535 } else {
536 return fetch_gitlab_ssh_keys(username, keys_out, num_keys);
537 }
538}
539
540// =============================================================================
541// Key Formatting Functions
542// =============================================================================
543
544asciichat_error_t parse_keys_from_file(const char *path, public_key_t *keys, size_t *num_keys, size_t max_keys) {
545 if (!path || !keys || !num_keys) {
546 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for key file parsing");
547 }
548
549 *num_keys = 0;
550
551 if (!path_looks_like_path(path)) {
552 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid keys file path: %s", path);
553 }
554
555 char *normalized_path = NULL;
556 asciichat_error_t path_result = path_validate_user_path(path, PATH_ROLE_CLIENT_KEYS, &normalized_path);
557 if (path_result != ASCIICHAT_OK) {
558 SAFE_FREE(normalized_path);
559 return path_result;
560 }
561
562 FILE *f = platform_fopen("file_stream", normalized_path, "r");
563 if (!f) {
564 SAFE_FREE(normalized_path);
565 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open keys file: %s", path);
566 }
567
568 char line[BUFFER_SIZE_LARGE];
569 while (fgets(line, sizeof(line), f) && *num_keys < max_keys) {
570 // Remove newline
571 line[strcspn(line, "\r\n")] = 0;
572
573 // Skip empty lines and comments
574 if (strlen(line) == 0 || line[0] == '#') {
575 continue;
576 }
577
578 if (parse_public_key(line, &keys[*num_keys]) == ASCIICHAT_OK) {
579 (*num_keys)++;
580 }
581 }
582
583 (void)fclose(f);
584 SAFE_FREE(normalized_path);
585 return ASCIICHAT_OK;
586}
587
588void format_public_key(const public_key_t *key, char *output, size_t output_size) {
589 if (!key || !output || output_size == 0) {
590 return;
591 }
592
593 if (key->type == KEY_TYPE_ED25519) {
594 // Format as SSH Ed25519 public key
595 // Simple base64 encoding for 32 bytes = 43 chars + padding
596 // For now, just use hex encoding
597 char hex_key[65];
598 for (size_t i = 0; i < 32; i++) {
599 char hex_byte[3];
600 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
601 hex_key[i * 2] = hex_byte[0];
602 hex_key[i * 2 + 1] = hex_byte[1];
603 }
604 hex_key[64] = '\0';
605 safe_snprintf(output, output_size, "ssh-ed25519 %s %s", hex_key, key->comment);
606 } else if (key->type == KEY_TYPE_X25519) {
607 // Format as hex X25519 key
608 char hex_key[65];
609 for (size_t i = 0; i < 32; i++) {
610 char hex_byte[3];
611 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
612 hex_key[i * 2] = hex_byte[0];
613 hex_key[i * 2 + 1] = hex_byte[1];
614 }
615 hex_key[64] = '\0';
616 safe_snprintf(output, output_size, "x25519 %s", hex_key);
617 } else {
618 safe_snprintf(output, output_size, "unknown key type: %d", key->type);
619 }
620}
621
622// =============================================================================
623// Hex Encoding/Decoding Utilities
624// =============================================================================
625
626asciichat_error_t hex_decode(const char *hex, uint8_t *output, size_t output_len) {
627 if (!hex || !output) {
628 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for hex_decode");
629 }
630
631 size_t hex_len = strlen(hex);
632 size_t expected_hex_len = output_len * 2;
633
634 if (hex_len != expected_hex_len) {
636 "Hex string length (%zu) doesn't match expected output length (%zu * 2 = %zu)", hex_len,
637 output_len, expected_hex_len);
638 }
639
640 // Decode hex string to binary
641 for (size_t i = 0; i < output_len; i++) {
642 char hex_byte[3] = {hex[i * 2], hex[i * 2 + 1], '\0'};
643 char *endptr;
644 unsigned long byte = strtoul(hex_byte, &endptr, 16);
645
646 // Validate hex character
647 if (*endptr != '\0' || byte > 255) {
648 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid hex character at position %zu: '%c%c'", i * 2, hex[i * 2],
649 hex[i * 2 + 1]);
650 }
651
652 output[i] = (uint8_t)byte;
653 }
654
655 return ASCIICHAT_OK;
656}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
⚙️ Common definitions, error codes, macros, and types shared throughout the application
GPG public key export interface.
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
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
unsigned char uint8_t
Definition common.h:56
int gpg_get_public_key(const char *key_id, uint8_t *public_key_out, char *keygrip_out)
Get public key from GPG keyring by key ID.
Definition export.c:248
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
#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
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
asciichat_error_t public_key_to_x25519(const public_key_t *key, uint8_t x25519_pk[32])
Convert public key to X25519 for Diffie-Hellman key exchange.
Definition keys.c:474
uint8_t key[32]
Definition key_types.h:71
asciichat_error_t parse_keys_from_file(const char *path, public_key_t *keys, size_t *num_keys, size_t max_keys)
Parse SSH keys from file (supports authorized_keys and known_hosts formats)
Definition keys.c:544
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
Parse SSH private key from file.
Definition keys.c:193
asciichat_error_t fetch_gitlab_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch GPG keys from GitLab using HTTPS.
Definition https_keys.c:262
char comment[256]
Definition key_types.h:72
asciichat_error_t ed25519_to_x25519_private(const uint8_t ed25519_sk[64], uint8_t x25519_sk[32])
Convert Ed25519 private key to X25519 for key exchange.
Definition ssh_keys.c:1013
asciichat_error_t private_key_to_x25519(const private_key_t *key, uint8_t x25519_sk[32])
Convert private key to X25519 for Diffie-Hellman key exchange.
Definition keys.c:493
asciichat_error_t fetch_github_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch SSH keys from GitHub using HTTPS.
Definition https_keys.c:166
asciichat_error_t ed25519_to_x25519_public(const uint8_t ed25519_pk[32], uint8_t x25519_pk[32])
Convert Ed25519 public key to X25519 for key exchange.
Definition ssh_keys.c:1000
key_type_t type
Definition key_types.h:92
char key_comment[256]
Definition key_types.h:100
asciichat_error_t parse_ssh_ed25519_line(const char *line, uint8_t ed25519_pk[32])
Parse SSH Ed25519 public key from "ssh-ed25519 AAAAC3..." format.
Definition ssh_keys.c:163
char gpg_keygrip[64]
Definition key_types.h:101
asciichat_error_t fetch_github_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch GPG keys from GitHub using HTTPS.
Definition https_keys.c:230
asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys)
Parse all SSH/GPG public keys from any format (returns all keys)
Definition keys.c:344
bool use_gpg_agent
Definition key_types.h:98
uint8_t public_key[32]
Definition key_types.h:99
asciichat_error_t fetch_gitlab_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch SSH keys from GitLab using HTTPS.
Definition https_keys.c:198
bool use_ssh_agent
Definition key_types.h:97
uint8_t ed25519[64]
Definition key_types.h:94
union private_key_t::@14 key
asciichat_error_t parse_gpg_key(const char *gpg_key_id, public_key_t *key_out)
Parse GPG key from armored text format.
Definition gpg_keys.c:25
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
Parse SSH/GPG public key from any format (returns first key only)
Definition keys.c:28
asciichat_error_t hex_decode(const char *hex, uint8_t *output, size_t output_len)
Decode hex string to binary (utility function for testing)
Definition keys.c:626
uint8_t x25519[32]
Definition key_types.h:95
asciichat_error_t parse_gpg_key_binary(const uint8_t *gpg_key_binary, size_t key_size, public_key_t *key_out)
Parse GPG key from binary format.
Definition gpg_keys.c:71
asciichat_error_t fetch_gitlab_keys(const char *username, char ***keys_out, size_t *num_keys, bool use_gpg)
Fetch SSH/GPG keys from GitLab using BearSSL.
Definition keys.c:528
asciichat_error_t parse_ssh_private_key(const char *key_path, private_key_t *key_out)
Parse SSH Ed25519 private key from file.
Definition ssh_keys.c:206
void format_public_key(const public_key_t *key, char *output, size_t output_size)
Convert public key to display format (ssh-ed25519 or x25519 hex)
Definition keys.c:588
key_type_t type
Definition key_types.h:70
asciichat_error_t fetch_github_keys(const char *username, char ***keys_out, size_t *num_keys, bool use_gpg)
Fetch SSH/GPG keys from GitHub using BearSSL.
Definition keys.c:516
@ KEY_TYPE_ED25519
Definition key_types.h:52
@ KEY_TYPE_X25519
Definition key_types.h:53
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
char * https_get(const char *hostname, const char *path)
Perform HTTPS GET request.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int platform_strncpy(char *dst, size_t dst_size, const char *src, size_t count)
Safe string copy with explicit size bounds (strncpy replacement)
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
char * platform_strtok_r(char *str, const char *delim, char **saveptr)
Thread-safe string tokenization (strtok_r replacement)
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:809
void url_parts_destroy(url_parts_t *parts)
Free parsed URL components.
Definition url.c:281
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:1020
asciichat_error_t url_parse(const char *url, url_parts_t *parts_out)
Parse HTTP(S), WebSocket, and TCP URL into components.
Definition url.c:166
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:321
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:320
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:319
Simple HTTPS client for fetching public keys from GitHub/GitLab.
OpenPGP (RFC 4880) packet format parser.
📂 Path Manipulation Utilities
Private key structure (for server –ssh-key)
Definition key_types.h:91
Public key structure.
Definition key_types.h:69
Structure containing parsed URL components.
Definition url.h:57
char * path
Extracted path, query, and fragment (allocated if present, may be NULL)
Definition url.h:63
char * host
Extracted hostname or IP address (allocated)
Definition url.h:60
🌐 Production-Grade URL Validation and Parsing