ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
known_hosts.c
Go to the documentation of this file.
1
7#include <stdio.h>
8#include <string.h>
9#include <stdlib.h>
10#include <errno.h>
11#include <fcntl.h>
12
13#include "crypto/known_hosts.h"
14#include "crypto/crypto.h" // Includes <sodium.h>, CRYPTO_* constants
15#include "common.h" // For BUFFER_SIZE_* constants
16#include "asciichat_errno.h" // For asciichat_errno system
17#include "crypto/keys.h"
18#include "util/ip.h"
19#include "platform/util.h"
20#include "platform/system.h" // For platform_isatty() and FILE_PERM_* constants
21#include "platform/fs.h" // For platform_mkdir(), platform_stat()
22#include "platform/question.h" // For platform_prompt_yes_no
23#include "options/options.h" // For opt_snapshot_mode
24#include "util/path.h"
25#include "util/string.h"
26#include "tooling/defer/defer.h"
27
28// Global variable to cache the expanded known_hosts path
29static char *g_known_hosts_path_cache = NULL;
30
46const char *get_known_hosts_path(void) {
47 // Return cached path if already determined
48 if (!g_known_hosts_path_cache) {
49 char *config_dir = get_config_dir();
50 if (!config_dir) {
51 log_error("Failed to determine configuration directory for known_hosts");
52 return NULL;
53 }
54
55 // Build path: config_dir + "known_hosts"
56 size_t config_len = strlen(config_dir);
57 size_t total_len = config_len + strlen("known_hosts") + 1;
58 char *path = SAFE_MALLOC(total_len, char *);
59 if (!path) {
60 SAFE_FREE(config_dir);
61 log_error("Failed to allocate memory for known_hosts path");
62 return NULL;
63 }
64
65 safe_snprintf(path, total_len, "%sknown_hosts", config_dir);
66 SAFE_FREE(config_dir);
67
68 g_known_hosts_path_cache = path;
69 log_debug("KNOWN_HOSTS: Using path %s", g_known_hosts_path_cache);
70 }
71 return g_known_hosts_path_cache;
72}
73
74// Format: IP:port x25519 <hex> [comment]
75// IPv4 example: 192.0.2.1:8080 x25519 1234abcd... ascii-chat
76// IPv6 example: [2001:db8::1]:8080 x25519 1234abcd... ascii-chat
77asciichat_error_t check_known_host(const char *server_ip, uint16_t port, const uint8_t server_key[32]) {
78 // Validate parameters first
79 if (!server_ip || !server_key) {
80 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
81 }
82
83 const char *path = get_known_hosts_path();
85 if (fd < 0) {
86 // File doesn't exist - this is an unknown host that needs verification
87 log_warn("Known hosts file does not exist: %s", path);
88 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
89 }
90 FILE *f = platform_fdopen(fd, "r");
92 if (!f) {
93 // Failed to open file descriptor as FILE*
95 log_warn("Failed to open known hosts file: %s", path);
96 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
97 }
98
99 char line[BUFFER_SIZE_XLARGE];
100 char expected_prefix[BUFFER_SIZE_MEDIUM];
101
102 // Format IP:port with proper bracket notation for IPv6
103 char ip_with_port[BUFFER_SIZE_MEDIUM];
104 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
105
106 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
107 }
108
109 // Add space after IP:port for prefix matching
110 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
111
112 // Search through ALL matching entries to find one that matches the server key
113 bool found_entries = false;
114 while (fgets(line, sizeof(line), f)) {
115 if (line[0] == '#')
116 continue; // Comment
117
118 if (strncmp(line, expected_prefix, strlen(expected_prefix)) == 0) {
119 // Found matching IP:port - check if this entry matches the server key
120 found_entries = true;
121 size_t prefix_len = strlen(expected_prefix);
122 size_t line_len = strlen(line);
123 if (line_len < prefix_len) {
124 // Line is too short to contain the prefix - skip this entry
125 continue;
126 }
127 char *key_type = line + prefix_len;
128
129 if (strncmp(key_type, NO_IDENTITY_MARKER, strlen(NO_IDENTITY_MARKER)) == 0) {
130 // This is a "no-identity" entry, but server is presenting an identity key
131 // This is a key mismatch - continue searching for a matching identity key
132 log_debug("SECURITY_DEBUG: Found no-identity entry, but server has identity key - continuing search");
133 continue;
134 }
135
136 // Parse key from line (normal identity key)
137 // Format: x25519 <hex_key> <comment>
138 // Extract just the hex key part
139 char *hex_key_start = strchr(key_type, ' ');
140 if (!hex_key_start) {
141 log_debug("SECURITY_DEBUG: No space found in key type: %s", key_type);
142 continue; // Try next entry
143 }
144 hex_key_start++; // Skip the space
145
146 // Find the end of the hex key (next space or end of line)
147 char *hex_key_end = strchr(hex_key_start, ' ');
148 if (hex_key_end) {
149 *hex_key_end = '\0'; // Null-terminate the hex key
150 }
151
152 public_key_t stored_key;
153 if (parse_public_key(hex_key_start, &stored_key) != 0) {
154 log_debug("SECURITY_DEBUG: Failed to parse key from hex: %s", hex_key_start);
155 continue; // Try next entry
156 }
157
158 // DEBUG: Print both keys for comparison
159 char server_key_hex[CRYPTO_HEX_KEY_SIZE_NULL], stored_key_hex[CRYPTO_HEX_KEY_SIZE_NULL];
160 for (int i = 0; i < CRYPTO_KEY_SIZE; i++) {
161 safe_snprintf(server_key_hex + i * 2, 3, "%02x", server_key[i]);
162 safe_snprintf(stored_key_hex + i * 2, 3, "%02x", stored_key.key[i]);
163 }
164 server_key_hex[CRYPTO_HEX_KEY_SIZE] = '\0';
165 stored_key_hex[CRYPTO_HEX_KEY_SIZE] = '\0';
166 log_debug("SECURITY_DEBUG: Server key: %s", server_key_hex);
167 log_debug("SECURITY_DEBUG: Stored key: %s", stored_key_hex);
168
169 // Check if server key is all zeros (no-identity server)
170 bool server_key_is_zero = true;
171 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
172 if (server_key[i] != 0) {
173 server_key_is_zero = false;
174 break;
175 }
176 }
177
178 // Check if stored key is all zeros
179 bool stored_key_is_zero = true;
180 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
181 if (stored_key.key[i] != 0) {
182 stored_key_is_zero = false;
183 break;
184 }
185 }
186
187 // If both keys are zero, this is a no-identity connection
188 // that was previously accepted by the user. Note: This provides weaker
189 // security than key-based verification since any server at this IP:port
190 // without an identity key will match.
191 if (server_key_is_zero && stored_key_is_zero) {
192 log_warn("SECURITY: Connecting to no-identity server at known IP:port. "
193 "This provides weaker security than key-based verification.");
194 return 1; // Match found (no-identity server)
195 }
196
197 // Compare keys (constant-time to prevent timing attacks)
198 if (sodium_memcmp(server_key, stored_key.key, ED25519_PUBLIC_KEY_SIZE) == 0) {
199 log_info("SECURITY: Server key matches known_hosts - connection verified");
200 return 1; // Match found!
201 }
202 log_debug("SECURITY_DEBUG: Key mismatch, continuing search...");
203 }
204 }
205
206 // No matching key found - check if we found any entries at all
207 if (found_entries) {
208 // We found entries for this IP:port but none matched the server key
209 // This is a key mismatch (potential MITM attack)
210 log_error("SECURITY: Server key does NOT match any known_hosts entries!");
211 log_error("SECURITY: This indicates a possible man-in-the-middle attack!");
212 return ERROR_CRYPTO_VERIFICATION; // Key mismatch - MITM warning!
213 }
214
215 // No entries found for this IP:port - first connection
216 return ASCIICHAT_OK; // Not found = first connection
217}
218
219// Check known_hosts for servers without identity key (no-identity entries)
220// Returns: 1 = previously accepted known host (no-identity entry found),
221// ASCIICHAT_OK = unknown host (first connection),
222// ERROR_CRYPTO_VERIFICATION = server changed from identity to no-identity
224 const char *path = get_known_hosts_path();
226 if (fd < 0) {
227 // File doesn't exist - this is an unknown host that needs verification
228 log_warn("Known hosts file does not exist: %s", path);
229 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
230 }
231
232 FILE *f = platform_fdopen(fd, "r");
233 defer(SAFE_FCLOSE(f));
234 if (!f) {
235 // Failed to open file descriptor as FILE*
236 platform_close(fd);
237 log_warn("Failed to open known hosts file: %s", path);
238 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
239 }
240
241 char line[BUFFER_SIZE_XLARGE];
242 char expected_prefix[BUFFER_SIZE_MEDIUM];
243
244 // Format IP:port with proper bracket notation for IPv6
245 char ip_with_port[BUFFER_SIZE_MEDIUM];
246 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
247
248 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
249 }
250
251 // Add space after IP:port for prefix matching
252 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
253
254 while (fgets(line, sizeof(line), f)) {
255 if (line[0] == '#')
256 continue; // Comment
257
258 if (strncmp(line, expected_prefix, strlen(expected_prefix)) == 0) {
259 // Found matching IP:port
260
261 // Check if this is a "no-identity" entry
262 // Bounds check: ensure line is long enough to contain the prefix
263 size_t prefix_len = strlen(expected_prefix);
264 size_t line_len = strlen(line);
265 if (line_len < prefix_len) {
266 // Line is too short to contain the prefix - this shouldn't happen
267 // but let's be safe and treat as unknown host
268 return ASCIICHAT_OK;
269 }
270 char *key_type = line + prefix_len;
271 // Skip leading whitespace
272 while (*key_type == ' ' || *key_type == '\t') {
273 key_type++;
274 }
275 if (strncmp(key_type, "no-identity", 11) == 0) {
276 // This is a server without identity key that was previously accepted by the user
277 // No warnings or user confirmation needed - user already accepted this server
278 return 1; // Known host (no-identity entry) - secure connection
279 }
280
281 // If we found a normal identity key entry, this is a mismatch
282 // Server previously had identity key but now has none
283 log_warn("Server previously had identity key but now has none - potential security issue");
284 return ERROR_CRYPTO_VERIFICATION; // Mismatch - server changed from identity to no-identity
285 }
286 }
287
288 return ASCIICHAT_OK; // Not found = first connection
289}
290
296static asciichat_error_t mkdir_recursive(const char *path) {
297 if (!path || !*path) {
298 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid path for mkdir_recursive");
299 }
300
301 // Make a mutable copy of the path
302 size_t len = strlen(path);
303 char *tmp = SAFE_MALLOC(len + 1, char *);
304 defer(SAFE_FREE(tmp));
305 if (!tmp) {
306 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for path");
307 }
308 memcpy(tmp, path, len + 1);
309
310 // Handle absolute paths (skip leading / or drive letter on Windows)
311 char *p = tmp;
312#ifdef _WIN32
313 // Skip drive letter (e.g., "C:")
314 if (len >= 2 && tmp[1] == ':') {
315 p += 2;
316 }
317#endif
318 // Skip leading separators (handle both / and \ on all platforms)
319 while (*p == '/' || *p == '\\') {
320 p++;
321 }
322
323 // Create directories one level at a time
324 for (; *p; p++) {
325 if (*p == '/' || *p == '\\') {
326 *p = '\0'; // Temporarily truncate
327
328 // Try to create this directory level using platform abstraction
330 if (mkdir_result != ASCIICHAT_OK) {
331 return mkdir_result;
332 }
333
334 *p = PATH_DELIM; // Restore path separator (use platform-specific separator)
335 }
336 }
337
338 // Create the final directory using platform abstraction
340 if (mkdir_result != ASCIICHAT_OK) {
341 return mkdir_result;
342 }
343
344 return ASCIICHAT_OK;
345}
346
347asciichat_error_t add_known_host(const char *server_ip, uint16_t port, const uint8_t server_key[32]) {
348 // Validate parameters first
349 if (!server_ip || !server_key) {
350 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
351 }
352
353 // Check for empty string (must be after NULL check)
354 size_t ip_len = strlen(server_ip);
355 if (ip_len == 0) {
356 return SET_ERRNO(ERROR_INVALID_PARAM, "Empty hostname/IP");
357 }
358
359 const char *path = get_known_hosts_path();
360 if (!path || path[0] == '\0') {
361 SET_ERRNO(ERROR_CONFIG, "Failed to get known hosts file path");
362 return ERROR_CONFIG;
363 }
364
365 // Create parent directories recursively (like mkdir -p)
366 size_t path_len = strlen(path);
367 if (path_len == 0) {
368 SET_ERRNO(ERROR_CONFIG, "Empty known hosts file path");
369 return ERROR_CONFIG;
370 }
371 char *dir = SAFE_MALLOC(path_len + 1, char *);
372 defer(SAFE_FREE(dir));
373 if (!dir) {
374 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
375 return ERROR_MEMORY;
376 }
377 memcpy(dir, path, path_len + 1);
378
379 // Find the last path separator
380 char *last_sep = strrchr(dir, PATH_DELIM);
381
382 if (last_sep) {
383 *last_sep = '\0'; // Truncate to get directory path
384 asciichat_error_t result = mkdir_recursive(dir);
385 if (result != ASCIICHAT_OK) {
386 return result; // Error already set by mkdir_recursive
387 }
388 }
389
390 // Create the file if it doesn't exist, then append to it
391 // Note: Temporarily removed log_debug to avoid potential crashes during debugging
392 // log_debug("KNOWN_HOSTS: Attempting to create/open file: %s", path);
393 // Use "a" mode for append-only (simpler and works better with chmod)
394 FILE *f = platform_fopen(path, "a");
395 defer(SAFE_FCLOSE(f));
396 if (!f) {
397 // log_debug("KNOWN_HOSTS: platform_fopen failed: %s (errno=%d)", SAFE_STRERROR(errno), errno);
398 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to create/open known hosts file: %s", path);
399 }
400 // Set secure permissions (0600) - only owner can read/write
401 // Note: chmod may fail if file was just created by fopen, but that's okay
403 // log_debug("KNOWN_HOSTS: Successfully opened file: %s", path);
404
405 // Format IP:port with proper bracket notation for IPv6
406 char ip_with_port[BUFFER_SIZE_MEDIUM];
407 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
408 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
409 }
410
411 // Convert key to hex for storage
412 char hex[CRYPTO_HEX_KEY_SIZE_NULL] = {0}; // Initialize to zeros for safety
413 bool is_placeholder = true;
414 // Build hex string byte by byte to avoid buffer overflow issues
415 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
416 // Convert each byte to 2 hex digits directly
417 uint8_t byte = server_key[i];
418 hex[i * 2] = "0123456789abcdef"[byte >> 4]; // High nibble
419 hex[i * 2 + 1] = "0123456789abcdef"[byte & 0xf]; // Low nibble
420 if (byte != 0) {
421 is_placeholder = false;
422 }
423 }
424 hex[CRYPTO_HEX_KEY_SIZE] = '\0'; // Ensure null termination (64 hex digits + null terminator)
425
426 // Write to file and check for errors
427 int fprintf_result;
428 if (is_placeholder) {
429 // Server has no identity key - store as placeholder
430 fprintf_result = safe_fprintf(f, "%s %s 0000000000000000000000000000000000000000000000000000000000000000 %s\n",
432 } else {
433 // Server has identity key - store normally
434 fprintf_result = safe_fprintf(f, "%s %s %s %s\n", ip_with_port, X25519_KEY_TYPE, hex, ASCII_CHAT_APP_NAME);
435 }
436
437 // Check if fprintf failed
438 if (fprintf_result < 0) {
439 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to write to known_hosts file: %s", path);
440 }
441
442 // Flush to ensure data is written
443 if (fflush(f) != 0) {
444 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to flush known_hosts file: %s", path);
445 }
446
447 log_debug("KNOWN_HOSTS: Successfully added host to known_hosts file: %s", path);
448
449 return ASCIICHAT_OK;
450}
451
452asciichat_error_t remove_known_host(const char *server_ip, uint16_t port) {
453 // Validate parameters first
454 if (!server_ip) {
455 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameter: server_ip=%p", server_ip);
456 }
457
458 const char *path = get_known_hosts_path();
460 if (fd < 0) {
461 // File doesn't exist - nothing to remove, return success
462 return ASCIICHAT_OK;
463 }
464 FILE *f = platform_fdopen(fd, "r");
465 defer(SAFE_FCLOSE(f));
466 if (!f) {
467 platform_close(fd);
468 SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
469 return ERROR_CONFIG;
470 }
471
472 // Format IP:port with proper bracket notation for IPv6
473 char ip_with_port[BUFFER_SIZE_MEDIUM];
474 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
475
476 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
477 }
478
479 // Read all lines into memory
480 char **lines = NULL;
481 size_t num_lines = 0;
482 defer({
483 if (lines) {
484 for (size_t i = 0; i < num_lines; i++) {
485 SAFE_FREE(lines[i]);
486 }
487 }
488 SAFE_FREE(lines);
489 });
490 char line[BUFFER_SIZE_XLARGE];
491
492 char expected_prefix[BUFFER_SIZE_MEDIUM];
493 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
494
495 while (fgets(line, sizeof(line), f)) {
496 // Skip lines that match this IP:port
497 if (strncmp(line, expected_prefix, strlen(expected_prefix)) != 0) {
498 // Keep this line
499 char **new_lines = SAFE_REALLOC((void *)lines, (num_lines + 1) * sizeof(char *), char **);
500 if (new_lines) {
501 lines = new_lines;
502 lines[num_lines] = platform_strdup(line);
503 if (lines[num_lines] == NULL) {
504 return SET_ERRNO(ERROR_MEMORY, "Failed to duplicate line from known_hosts file");
505 }
506 num_lines++;
507 }
508 }
509 }
510 // Close first file before opening for write
511 if (f) {
512 fclose(f);
513 f = NULL; // Prevent double-close by defer
514 }
515
516 // Write back the filtered lines
518 f = platform_fdopen(fd, "w");
519 if (!f) {
520 // Cleanup on error - fdopen failed, so fd is still open but f is NULL
521 // Individual line strings will be freed by defer cleanup
522 platform_close(fd); // Close fd directly since fdopen failed
523 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
524 }
525
526 for (size_t i = 0; i < num_lines; i++) {
527 (void)fputs(lines[i], f);
528 }
529 // All cleanup (lines, individual strings, file handle) handled by defer statements
530
531 log_debug("KNOWN_HOSTS: Successfully removed host from known_hosts file: %s", path);
532 return ASCIICHAT_OK;
533}
534
535// Compute SHA256 fingerprint of key for display
538 crypto_hash_sha256(hash, key, ED25519_PUBLIC_KEY_SIZE);
539
540 // Build hex string byte by byte to avoid buffer overflow issues
541 for (int i = 0; i < HMAC_SHA256_SIZE; i++) {
542 uint8_t byte = hash[i];
543 fingerprint[i * 2] = "0123456789abcdef"[byte >> 4]; // High nibble
544 fingerprint[i * 2 + 1] = "0123456789abcdef"[byte & 0xf]; // Low nibble
545 }
546 fingerprint[CRYPTO_HEX_KEY_SIZE] = '\0';
547}
548
549// Interactive prompt for unknown host - returns true if user wants to add, false to abort
550bool prompt_unknown_host(const char *server_ip, uint16_t port, const uint8_t server_key[32]) {
551 char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL];
552 compute_key_fingerprint(server_key, fingerprint);
553
554 // Format IP:port with proper bracket notation for IPv6
555 char ip_with_port[BUFFER_SIZE_MEDIUM];
556 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
557 // Fallback to basic format if error
558 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
559 }
560
561 // Check if we're running interactively (stdin is a terminal and not in snapshot mode)
562 const char *env_skip_known_hosts_checking = platform_getenv("ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK");
563 if (env_skip_known_hosts_checking && strcmp(env_skip_known_hosts_checking, STR_ONE) == 0) {
564 log_warn("Skipping known_hosts checking. This is a security vulnerability.");
565 return true;
566 }
567#ifndef NDEBUG
568 // In debug builds, also skip for Claude Code (LLM automation can't do interactive prompts)
569 const char *env_claudecode = platform_getenv("CLAUDECODE");
570 if (env_claudecode && strlen(env_claudecode) > 0) {
571 log_warn("Skipping known_hosts checking (CLAUDECODE set in debug build).");
572 return true;
573 }
574#endif
575 if (!platform_isatty(STDIN_FILENO) || GET_OPTION(snapshot_mode)) {
576 // SECURITY: Non-interactive mode - REJECT unknown hosts to prevent MITM attacks
577 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify unknown host in non-interactive mode");
578 log_error("ERROR: Cannot verify unknown host in non-interactive mode without environment variable bypass.\n"
579 "This connection may be a man-in-the-middle attack!\n"
580 "\n"
581 "To connect to this host:\n"
582 " 1. Run the client interactively (from a terminal with TTY)\n"
583 " 2. Verify the fingerprint: SHA256:%s\n"
584 " 3. Accept the host when prompted\n"
585 " 4. The host will be added to: %s\n"
586 "\n"
587 "Connection aborted for security.\n"
588 "To bypass this check, set the environment variable ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK to 1",
589 fingerprint, get_known_hosts_path());
590 return false; // REJECT unknown hosts in non-interactive mode
591 }
592
593 // Interactive mode - prompt user
594 // Lock terminal so only this thread can output to terminal
595 // Other threads' logs are buffered until we unlock
596 bool previous_terminal_state = log_lock_terminal();
597
598 log_plain("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
599 "@ WARNING: REMOTE HOST IDENTIFICATION NOT KNOWN! @\n"
600 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
601 "\n"
602 "The authenticity of host '%s' can't be established.\n"
603 "Ed25519 key fingerprint is SHA256:%s\n",
604 ip_with_port, fingerprint);
605
606 // Unlock before prompt (prompt_yes_no handles its own terminal locking)
607 log_unlock_terminal(previous_terminal_state);
608
609 // Prompt user - default is No for security
610 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
611 log_warn("Warning: Permanently added '%s' to the list of known hosts.", ip_with_port);
612 return true;
613 }
614
615 log_warn("Connection aborted by user.");
616 return false;
617}
618
619// Display MITM warning with key comparison and prompt user for confirmation
620// Returns true if user accepts the risk and wants to continue, false otherwise
621bool display_mitm_warning(const char *server_ip, uint16_t port, const uint8_t expected_key[32],
622 const uint8_t received_key[32]) {
623 char expected_fp[CRYPTO_HEX_KEY_SIZE_NULL], received_fp[CRYPTO_HEX_KEY_SIZE_NULL];
624 compute_key_fingerprint(expected_key, expected_fp);
625 compute_key_fingerprint(received_key, received_fp);
626
627 const char *known_hosts_path = get_known_hosts_path();
628
629 // Format IP:port with proper bracket notation for IPv6
630 char ip_with_port[BUFFER_SIZE_MEDIUM];
631 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
632 // Fallback to basic format if error
633 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
634 }
635
636 char escaped_ip_with_port[128];
637 escape_ascii(ip_with_port, "[]", escaped_ip_with_port, 128);
638 log_warn("\n"
639 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
640 "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n"
641 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
642 "\n"
643 "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"
644 "Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"
645 "It is also possible that the host key has just been changed.\n"
646 "\n"
647 "The fingerprint for the Ed25519 key sent by the remote host is:\n"
648 "SHA256:%s\n"
649 "\n"
650 "Expected fingerprint:\n"
651 "SHA256:%s\n"
652 "\n"
653 "Please contact your system administrator.\n"
654 "\n"
655 "Add correct host key in %s to get rid of this message.\n"
656 "Offending key for IP address %s was found at:\n"
657 "%s\n"
658 "\n"
659 "To update the key, run:\n"
660 " # Linux/macOS:\n"
661 " sed -i '' '/%s /d' ~/.ascii-chat/known_hosts\n"
662 " # or run this instead:\n"
663 " cat ~/.ascii-chat/known_hosts | grep -v '%s ' > /tmp/x; cp /tmp/x ~/.ascii-chat/known_hosts\n"
664 " # Windows PowerShell:\n"
665 " (Get-Content ~/.ascii-chat/known_hosts) | Where-Object { $_ -notmatch '^%s ' } | Set-Content "
666 "~/.ascii-chat/known_hosts\n"
667 " # Or manually edit ~/.ascii-chat/known_hosts to remove lines starting with '%s '\n"
668 "\n"
669 "Host key verification failed.\n"
670 "\n",
671 received_fp, expected_fp, known_hosts_path, ip_with_port, known_hosts_path, ip_with_port,
672 escaped_ip_with_port, ip_with_port, ip_with_port);
673
674 return false;
675}
676
677// Interactive prompt for unknown host without identity key
678// Returns true if user wants to continue, false to abort
679bool prompt_unknown_host_no_identity(const char *server_ip, uint16_t port) {
680 // Format IP:port with proper bracket notation for IPv6
681 char ip_with_port[BUFFER_SIZE_MEDIUM];
682 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
683 // Fallback to basic format if error
684 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
685 }
686
687 log_warn("\n"
688 "The authenticity of host '%s' can't be established.\n"
689 "The server has no identity key to verify its authenticity.\n"
690 "\n"
691 "WARNING: This connection is vulnerable to man-in-the-middle attacks!\n"
692 "Anyone can intercept your connection and read your data.\n"
693 "\n"
694 "To secure this connection:\n"
695 " 1. Server should use --key to provide an identity key\n"
696 " 2. Client should use --server-key to verify the server\n"
697 "\n",
698 ip_with_port);
699
700 // Check if we're running interactively (stdin is a terminal and not in snapshot mode)
701 if (!platform_isatty(STDIN_FILENO) || GET_OPTION(snapshot_mode)) {
702 // SECURITY: Non-interactive mode - REJECT unknown hosts without identity
703 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify server without identity key in non-interactive mode");
704 log_error("ERROR: Cannot verify server without identity key in non-interactive mode.\n"
705 "ERROR: This connection is vulnerable to man-in-the-middle attacks!\n"
706 "\n"
707 "To connect to this host:\n"
708 " 1. Run the client interactively (from a terminal with TTY)\n"
709 " 2. Verify you trust this server despite no identity key\n"
710 " 3. Accept the risk when prompted\n"
711 " OR better: Ask server admin to use --key for proper authentication\n"
712 "\n"
713 "Connection aborted for security.\n"
714 "\n");
715 return false; // REJECT unknown hosts without identity in non-interactive mode
716 }
717
718 // Interactive mode - prompt user (default is No for security)
719 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
720 log_warn("Warning: Proceeding with unverified connection.\n"
721 "Your data may be intercepted by attackers!\n"
722 "\n");
723 return true;
724 }
725
726 log_plain("Connection aborted by user.");
727 return false;
728}
729
730// Cleanup function to free the cached known_hosts path
732 if (g_known_hosts_path_cache) {
733 SAFE_FREE(g_known_hosts_path_cache);
734 g_known_hosts_path_cache = NULL;
735 }
736}
⚠️‼️ Error and/or exit() when things go bad.
Defer macro definition for source-to-source transformation.
Cross-platform file system operations.
#define BUFFER_SIZE_XLARGE
Extra large buffer size (2048 bytes)
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes)
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:228
unsigned short uint16_t
Definition common.h:57
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
unsigned char uint8_t
Definition common.h:56
#define SAFE_FCLOSE(fp)
Definition common.h:330
#define ASCII_CHAT_APP_NAME
Application name for key comments ("ascii-chat")
Definition common.h:43
bool prompt_unknown_host(const char *server_ip, uint16_t port, const uint8_t server_key[32])
Interactive prompt for unknown host - returns true if user wants to add, false to abort.
#define CRYPTO_KEY_SIZE
Ed25519 key size in bytes.
void known_hosts_cleanup(void)
Cleanup function to free cached known_hosts path.
#define CRYPTO_HEX_KEY_SIZE
Hex string size for 32-byte key (64 hex characters)
const char * get_known_hosts_path(void)
Get the path to the known_hosts file.
Definition known_hosts.c:46
#define NO_IDENTITY_MARKER
No-identity entry marker ("no-identity")
#define HMAC_SHA256_SIZE
HMAC-SHA256 output size in bytes.
bool prompt_unknown_host_no_identity(const char *server_ip, uint16_t port)
Interactive prompt for unknown host without identity key - returns true if user wants to continue,...
#define ED25519_PUBLIC_KEY_SIZE
Ed25519 public key size in bytes.
#define CRYPTO_HEX_KEY_SIZE_NULL
Hex string size for 32-byte key with null terminator (65 bytes)
asciichat_error_t check_known_host_no_identity(const char *server_ip, uint16_t port)
Check known_hosts for servers without identity key (no-identity entries)
asciichat_error_t add_known_host(const char *server_ip, uint16_t port, const uint8_t server_key[32])
Add server to known_hosts.
#define X25519_KEY_TYPE
X25519 key type string ("x25519")
asciichat_error_t remove_known_host(const char *server_ip, uint16_t port)
Remove server from known_hosts.
bool display_mitm_warning(const char *server_ip, uint16_t port, const uint8_t expected_key[32], const uint8_t received_key[32])
Display MITM warning with key comparison and prompt user for confirmation.
asciichat_error_t check_known_host(const char *server_ip, uint16_t port, const uint8_t server_key[32])
Check if server key is in known_hosts.
Definition known_hosts.c:77
#define defer(action)
Defer a cleanup action until function scope exit.
Definition defer.h:36
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context.
#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_VERIFICATION
Definition error_codes.h:92
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_CONFIG
Definition error_codes.h:54
@ ERROR_CRYPTO
Definition error_codes.h:88
@ ERROR_INVALID_PARAM
uint8_t key[32]
Definition key_types.h:71
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
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
bool log_lock_terminal(void)
Lock terminal output for exclusive access by the calling thread.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
void log_unlock_terminal(bool previous_state)
Release terminal lock and flush buffered messages.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
Definition options.h:644
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition system.h:649
int safe_fprintf(FILE *stream, const char *format,...)
Safe version of fprintf.
FILE * platform_fopen(const char *filename, const char *mode)
Safe file open stream (fopen replacement)
asciichat_error_t platform_mkdir(const char *path, int mode)
Create a directory.
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
bool platform_prompt_yes_no(const char *prompt, bool default_yes)
Prompt the user for a yes/no answer.
#define PATH_DELIM
Platform-specific path separator character.
Definition system.h:605
int platform_chmod(const char *pathname, int mode)
Change file permissions/mode.
#define PLATFORM_O_CREAT
Create file if it doesn't exist.
Definition file.h:47
#define PLATFORM_O_TRUNC
Truncate file to zero length if it exists.
Definition file.h:49
const char * platform_getenv(const char *name)
Get an environment variable value.
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
FILE * platform_fdopen(int fd, const char *mode)
Convert file descriptor to stream (fdopen replacement)
int platform_close(int fd)
Safe file close (close replacement)
#define PLATFORM_O_WRONLY
Open file for writing only.
Definition file.h:45
int platform_open(const char *pathname, int flags,...)
Safe file open (open replacement)
#define FILE_PERM_PRIVATE
File permission: Private (owner read/write only)
Definition system.h:637
#define PLATFORM_O_RDONLY
Open file for reading only.
Definition file.h:44
#define STR_ONE
String literal: "1" (one)
asciichat_error_t format_ip_with_port(const char *ip, uint16_t port, char *output, size_t output_size)
Format IP address with port number.
Definition ip.c:230
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:223
void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
Escape ASCII characters in a string.
Definition string.c:12
🌍 IP Address Parsing and Formatting Utilities
void compute_key_fingerprint(const uint8_t key[ED25519_PUBLIC_KEY_SIZE], char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL])
Known hosts management for MITM attack prevention.
⚙️ Command-line options parsing and configuration management for ascii-chat
📂 Path Manipulation Utilities
Public platform utility API for string, memory, and file operations.
Cross-platform interactive prompting utilities.
Public key structure.
Definition key_types.h:69
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities