ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
keys.c
Go to the documentation of this file.
1
7#include "keys.h"
8#include "key_types.h"
9#include "ssh/ssh_keys.h"
10#include "gpg/gpg_keys.h"
11#include "https_keys.h"
12#include "common.h"
13#include "asciichat_errno.h"
14#include "util/path.h"
15#include "gpg/export.h" // For gpg_get_public_key()
16#include <string.h>
17#include <stdlib.h>
18#include <stdio.h>
19
20// =============================================================================
21// High-Level Key Parsing Functions
22// =============================================================================
23
24asciichat_error_t parse_public_key(const char *input, public_key_t *key_out) {
25 if (!input || !key_out) {
26 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key parsing");
27 }
28
29 // Clear output structure
30 memset(key_out, 0, sizeof(public_key_t));
31
32 // Try SSH key parsing first
33 if (strncmp(input, "ssh-ed25519", 11) == 0) {
34 key_out->type = KEY_TYPE_ED25519;
35 asciichat_error_t result = parse_ssh_ed25519_line(input, key_out->key);
36 if (result == ASCIICHAT_OK) {
37 platform_strncpy(key_out->comment, sizeof(key_out->comment), "ssh-ed25519", sizeof(key_out->comment) - 1);
38 }
39 return result;
40 }
41
42 // Try GPG key parsing
43 if (strncmp(input, "gpg:", 4) == 0) {
44 return parse_gpg_key(input + 4, key_out);
45 }
46
47 // Try HTTPS key fetching (GitHub/GitLab) - delegate to parse_public_keys and return first
48 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
49 public_key_t keys[1];
50 size_t num_keys = 0;
51 asciichat_error_t result = parse_public_keys(input, keys, &num_keys, 1);
52 if (result == ASCIICHAT_OK && num_keys > 0) {
53 *key_out = keys[0];
54 }
55 return result;
56 }
57
58 // Try raw hex key (64 hex chars = 32 bytes)
59 if (strlen(input) == 64) {
60 // Check if it's valid hex
61 bool is_valid_hex = true;
62 for (int i = 0; i < 64; i++) {
63 if (!((input[i] >= '0' && input[i] <= '9') || (input[i] >= 'a' && input[i] <= 'f') ||
64 (input[i] >= 'A' && input[i] <= 'F'))) {
65 is_valid_hex = false;
66 break;
67 }
68 }
69
70 if (is_valid_hex) {
71 // Assume it's a raw X25519 public key in hex (default for raw hex)
72 key_out->type = KEY_TYPE_X25519;
73 // Use hex_decode utility to safely decode hex string to binary
74 if (hex_decode(input, key_out->key, 32) != ASCIICHAT_OK) {
75 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode hex key");
76 }
77 platform_strncpy(key_out->comment, sizeof(key_out->comment), "raw-hex", sizeof(key_out->comment) - 1);
78 return ASCIICHAT_OK;
79 }
80 }
81
82 if (path_looks_like_path(input)) {
83 char *normalized_path = NULL;
84 asciichat_error_t path_result = path_validate_user_path(input, PATH_ROLE_KEY_PUBLIC, &normalized_path);
85 if (path_result != ASCIICHAT_OK) {
86 SAFE_FREE(normalized_path);
87 return path_result;
88 }
89
90 FILE *f = platform_fopen(normalized_path, "r");
91 if (f) {
92 char line[BUFFER_SIZE_LARGE];
93 if (fgets(line, sizeof(line), f)) {
94 (void)fclose(f);
95 SAFE_FREE(normalized_path);
96 // Remove newline
97 line[strcspn(line, "\r\n")] = 0;
98 return parse_public_key(line, key_out);
99 }
100 (void)fclose(f);
101 }
102 SAFE_FREE(normalized_path);
103 }
104
105 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key format: %s", input);
106}
107
108asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out) {
109 if (!key_path || !key_out) {
110 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private key parsing");
111 }
112
113 // Clear output structure
114 memset(key_out, 0, sizeof(private_key_t));
115
116 // Check for GPG key format: "gpg:KEYID" where KEYID is 8, 16, or 40 hex chars
117 // - 8 chars: short key ID (last 8 chars of fingerprint)
118 // - 16 chars: long key ID (last 16 chars of fingerprint)
119 // - 40 chars: full fingerprint
120 if (strncmp(key_path, "gpg:", 4) == 0) {
121 const char *key_id = key_path + 4; // Skip "gpg:" prefix
122
123 // Validate key ID format (must be 8, 16, or 40 hex characters)
124 size_t key_id_len = strlen(key_id);
125 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
126 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)",
127 key_id_len);
128 }
129
130 // Validate hex characters
131 for (size_t i = 0; i < key_id_len; i++) {
132 char c = key_id[i];
133 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
134 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID: contains non-hex character '%c'", c);
135 }
136 }
137
138 // Get public key and keygrip from GPG keyring
139 uint8_t public_key[32];
140 char keygrip[64];
141 if (gpg_get_public_key(key_id, public_key, keygrip) != 0) {
142 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to get public key from GPG for key ID: %s", key_id);
143 }
144
145 // Populate private_key_t structure for GPG agent use
146 key_out->type = KEY_TYPE_ED25519;
147 key_out->use_gpg_agent = true;
148 key_out->use_ssh_agent = false;
149
150 // Store public key (for handshake and verification)
151 memcpy(key_out->public_key, public_key, 32);
152
153 // Store keygrip (for GPG agent signing operations)
154 platform_strncpy(key_out->gpg_keygrip, sizeof(key_out->gpg_keygrip), keygrip, sizeof(key_out->gpg_keygrip) - 1);
155
156 // Store comment for display
157 safe_snprintf(key_out->key_comment, sizeof(key_out->key_comment), "GPG key %s", key_id);
158
159 // Clear the key.ed25519 field (we don't have the private key in memory)
160 // The private key stays protected in GPG agent
161 memset(key_out->key.ed25519, 0, 64);
162
163 // Set the public key in the second half of ed25519 key (standard Ed25519 format)
164 // This is for compatibility with code that expects the public key at offset 32
165 memcpy(key_out->key.ed25519 + 32, public_key, 32);
166
167 log_info("Loaded GPG key %s (keygrip: %.40s) for agent signing", key_id, keygrip);
168 return ASCIICHAT_OK;
169 }
170
171 // Try SSH private key parsing
172 char *normalized_path = NULL;
173 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
174 if (path_result != ASCIICHAT_OK) {
175 SAFE_FREE(normalized_path);
176 return path_result;
177 }
178 asciichat_error_t result = parse_ssh_private_key(normalized_path, key_out);
179 SAFE_FREE(normalized_path);
180 return result;
181}
182
183// =============================================================================
184// Multi-Key Public Key Parsing
185// =============================================================================
186
187asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys) {
188 if (!input || !keys_out || !num_keys || max_keys == 0) {
189 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for multi-key parsing");
190 }
191
192 *num_keys = 0;
193
194 // Check for direct SSH Ed25519 key format BEFORE checking for file paths
195 // (SSH keys can contain '/' in base64, which would match path_looks_like_path)
196 if (strncmp(input, "ssh-ed25519", 11) == 0) {
197 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
198 if (result == ASCIICHAT_OK) {
199 *num_keys = 1;
200 }
201 return result;
202 }
203
204 // Check if this is a GitHub/GitLab reference - these support multiple keys
205 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
206 const char *username = input + 7; // Skip "github:" or "gitlab:"
207 bool is_github = (strncmp(input, "github:", 7) == 0);
208 bool is_gpg = (strstr(username, ".gpg") != NULL);
209
210 char **keys = NULL;
211 size_t num_fetched_keys = 0;
212 asciichat_error_t result;
213
214 if (is_github) {
215 if (is_gpg) {
216 result = fetch_github_gpg_keys(username, &keys, &num_fetched_keys);
217 } else {
218 result = fetch_github_ssh_keys(username, &keys, &num_fetched_keys);
219 }
220 } else {
221 if (is_gpg) {
222 result = fetch_gitlab_gpg_keys(username, &keys, &num_fetched_keys);
223 } else {
224 result = fetch_gitlab_ssh_keys(username, &keys, &num_fetched_keys);
225 }
226 }
227
228 if (result != ASCIICHAT_OK || num_fetched_keys == 0) {
229 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch keys from %s for user: %s", is_github ? "GitHub" : "GitLab",
230 username);
231 }
232
233 // Parse each fetched key (only Ed25519 keys will succeed)
234 for (size_t i = 0; i < num_fetched_keys && *num_keys < max_keys; i++) {
235 if (parse_public_key(keys[i], &keys_out[*num_keys]) == ASCIICHAT_OK) {
236 (*num_keys)++;
237 }
238 // Non-Ed25519 keys are silently skipped
239 }
240
241 // Free the keys array
242 for (size_t i = 0; i < num_fetched_keys; i++) {
243 SAFE_FREE(keys[i]);
244 }
245 SAFE_FREE(keys);
246
247 if (*num_keys == 0) {
248 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid Ed25519 keys found for %s user: %s", is_github ? "GitHub" : "GitLab",
249 username);
250 }
251
252 log_info("Parsed %zu Ed25519 key(s) from %s user: %s", *num_keys, is_github ? "GitHub" : "GitLab", username);
253 return ASCIICHAT_OK;
254 }
255
256 // For file paths, delegate to parse_keys_from_file
257 if (path_looks_like_path(input)) {
258 return parse_keys_from_file(input, keys_out, num_keys, max_keys);
259 }
260
261 // For all other formats, use single-key parsing
262 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
263 if (result == ASCIICHAT_OK) {
264 *num_keys = 1;
265 }
266 return result;
267}
268
269// =============================================================================
270// Key Conversion Functions
271// =============================================================================
272
274 if (!key || !x25519_pk) {
275 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public_key_to_x25519");
276 }
277
278 if (key->type == KEY_TYPE_X25519) {
279 // Passthrough for X25519 keys
280 memcpy(x25519_pk, key->key, 32);
281 return ASCIICHAT_OK;
282 }
283
284 if (key->type == KEY_TYPE_ED25519) {
285 // Convert Ed25519 to X25519
286 return ed25519_to_x25519_public(key->key, x25519_pk);
287 }
288
289 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
290}
291
293 if (!key || !x25519_sk) {
294 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private_key_to_x25519");
295 }
296
297 if (key->type == KEY_TYPE_X25519) {
298 // Passthrough for X25519 keys
299 memcpy(x25519_sk, key->key.x25519, 32);
300 return ASCIICHAT_OK;
301 }
302
303 if (key->type == KEY_TYPE_ED25519) {
304 // Convert Ed25519 to X25519 (Ed25519 private key is 64 bytes: seed + public)
305 return ed25519_to_x25519_private(key->key.ed25519, x25519_sk);
306 }
307
308 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
309}
310
311// =============================================================================
312// HTTPS Key Fetching Wrapper Functions
313// =============================================================================
314
315asciichat_error_t fetch_github_keys(const char *username, char ***keys_out, size_t *num_keys, bool use_gpg) {
316 if (!username || !keys_out || !num_keys) {
317 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_github_keys");
318 }
319
320 if (use_gpg) {
321 return fetch_github_gpg_keys(username, keys_out, num_keys);
322 } else {
323 return fetch_github_ssh_keys(username, keys_out, num_keys);
324 }
325}
326
327asciichat_error_t fetch_gitlab_keys(const char *username, char ***keys_out, size_t *num_keys, bool use_gpg) {
328 if (!username || !keys_out || !num_keys) {
329 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_gitlab_keys");
330 }
331
332 if (use_gpg) {
333 return fetch_gitlab_gpg_keys(username, keys_out, num_keys);
334 } else {
335 return fetch_gitlab_ssh_keys(username, keys_out, num_keys);
336 }
337}
338
339// =============================================================================
340// Key Formatting Functions
341// =============================================================================
342
343asciichat_error_t parse_keys_from_file(const char *path, public_key_t *keys, size_t *num_keys, size_t max_keys) {
344 if (!path || !keys || !num_keys) {
345 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for key file parsing");
346 }
347
348 *num_keys = 0;
349
350 if (!path_looks_like_path(path)) {
351 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid keys file path: %s", path);
352 }
353
354 char *normalized_path = NULL;
355 asciichat_error_t path_result = path_validate_user_path(path, PATH_ROLE_CLIENT_KEYS, &normalized_path);
356 if (path_result != ASCIICHAT_OK) {
357 SAFE_FREE(normalized_path);
358 return path_result;
359 }
360
361 FILE *f = platform_fopen(normalized_path, "r");
362 if (!f) {
363 SAFE_FREE(normalized_path);
364 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open keys file: %s", path);
365 }
366
367 char line[BUFFER_SIZE_LARGE];
368 while (fgets(line, sizeof(line), f) && *num_keys < max_keys) {
369 // Remove newline
370 line[strcspn(line, "\r\n")] = 0;
371
372 // Skip empty lines and comments
373 if (strlen(line) == 0 || line[0] == '#') {
374 continue;
375 }
376
377 if (parse_public_key(line, &keys[*num_keys]) == ASCIICHAT_OK) {
378 (*num_keys)++;
379 }
380 }
381
382 (void)fclose(f);
383 SAFE_FREE(normalized_path);
384 return ASCIICHAT_OK;
385}
386
387void format_public_key(const public_key_t *key, char *output, size_t output_size) {
388 if (!key || !output || output_size == 0) {
389 return;
390 }
391
392 if (key->type == KEY_TYPE_ED25519) {
393 // Format as SSH Ed25519 public key
394 // Simple base64 encoding for 32 bytes = 43 chars + padding
395 // For now, just use hex encoding
396 char hex_key[65];
397 for (size_t i = 0; i < 32; i++) {
398 char hex_byte[3];
399 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
400 hex_key[i * 2] = hex_byte[0];
401 hex_key[i * 2 + 1] = hex_byte[1];
402 }
403 hex_key[64] = '\0';
404 safe_snprintf(output, output_size, "ssh-ed25519 %s %s", hex_key, key->comment);
405 } else if (key->type == KEY_TYPE_X25519) {
406 // Format as hex X25519 key
407 char hex_key[65];
408 for (size_t i = 0; i < 32; i++) {
409 char hex_byte[3];
410 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
411 hex_key[i * 2] = hex_byte[0];
412 hex_key[i * 2 + 1] = hex_byte[1];
413 }
414 hex_key[64] = '\0';
415 safe_snprintf(output, output_size, "x25519 %s", hex_key);
416 } else {
417 safe_snprintf(output, output_size, "unknown key type: %d", key->type);
418 }
419}
420
421// =============================================================================
422// Hex Encoding/Decoding Utilities
423// =============================================================================
424
425asciichat_error_t hex_decode(const char *hex, uint8_t *output, size_t output_len) {
426 if (!hex || !output) {
427 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for hex_decode");
428 }
429
430 size_t hex_len = strlen(hex);
431 size_t expected_hex_len = output_len * 2;
432
433 if (hex_len != expected_hex_len) {
435 "Hex string length (%zu) doesn't match expected output length (%zu * 2 = %zu)", hex_len,
436 output_len, expected_hex_len);
437 }
438
439 // Decode hex string to binary
440 for (size_t i = 0; i < output_len; i++) {
441 char hex_byte[3] = {hex[i * 2], hex[i * 2 + 1], '\0'};
442 char *endptr;
443 unsigned long byte = strtoul(hex_byte, &endptr, 16);
444
445 // Validate hex character
446 if (*endptr != '\0' || byte > 255) {
447 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid hex character at position %zu: '%c%c'", i * 2, hex[i * 2],
448 hex[i * 2 + 1]);
449 }
450
451 output[i] = (uint8_t)byte;
452 }
453
454 return ASCIICHAT_OK;
455}
⚠️‼️ Error and/or exit() when things go bad.
GPG public key export interface.
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
#define SAFE_FREE(ptr)
Definition common.h:320
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:251
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_CRYPTO_KEY
Definition error_codes.h:89
@ ASCIICHAT_OK
Definition error_codes.h:48
@ 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:273
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:343
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
Parse SSH private key from file.
Definition keys.c:108
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:274
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:1050
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:292
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:178
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:1037
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:167
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:242
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:187
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:210
bool use_ssh_agent
Definition key_types.h:97
uint8_t ed25519[64]
Definition key_types.h:94
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:32
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:24
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:425
uint8_t x25519[32]
Definition key_types.h:95
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:327
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:218
union private_key_t::@1 key
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:387
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:315
@ KEY_TYPE_ED25519
Definition key_types.h:52
@ KEY_TYPE_X25519
Definition key_types.h:53
#define log_info(...)
Log an INFO message.
FILE * platform_fopen(const char *filename, const char *mode)
Safe file open stream (fopen replacement)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
int platform_strncpy(char *dst, size_t dst_size, const char *src, size_t count)
Safe string copy with explicit size bounds (strncpy replacement)
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:439
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:498
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:258
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:257
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:256
📂 Path Manipulation Utilities
Private key structure (for server –ssh-key)
Definition key_types.h:91
Public key structure.
Definition key_types.h:69