29static char *g_known_hosts_path_cache = NULL;
48 if (!g_known_hosts_path_cache) {
51 log_error(
"Failed to determine configuration directory for known_hosts");
56 size_t config_len = strlen(config_dir);
57 size_t total_len = config_len + strlen(
"known_hosts") + 1;
61 log_error(
"Failed to allocate memory for known_hosts path");
68 g_known_hosts_path_cache = path;
69 log_debug(
"KNOWN_HOSTS: Using path %s", g_known_hosts_path_cache);
71 return g_known_hosts_path_cache;
79 if (!server_ip || !server_key) {
87 log_warn(
"Known hosts file does not exist: %s", path);
95 log_warn(
"Failed to open known hosts file: %s", path);
110 safe_snprintf(expected_prefix,
sizeof(expected_prefix),
"%s ", ip_with_port);
113 bool found_entries =
false;
114 while (fgets(line,
sizeof(line), f)) {
118 if (strncmp(line, expected_prefix, strlen(expected_prefix)) == 0) {
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) {
127 char *key_type = line + prefix_len;
132 log_debug(
"SECURITY_DEBUG: Found no-identity entry, but server has identity key - continuing search");
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);
147 char *hex_key_end = strchr(hex_key_start,
' ');
154 log_debug(
"SECURITY_DEBUG: Failed to parse key from hex: %s", hex_key_start);
161 safe_snprintf(server_key_hex + i * 2, 3,
"%02x", server_key[i]);
166 log_debug(
"SECURITY_DEBUG: Server key: %s", server_key_hex);
167 log_debug(
"SECURITY_DEBUG: Stored key: %s", stored_key_hex);
170 bool server_key_is_zero =
true;
172 if (server_key[i] != 0) {
173 server_key_is_zero =
false;
179 bool stored_key_is_zero =
true;
181 if (stored_key.
key[i] != 0) {
182 stored_key_is_zero =
false;
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.");
199 log_info(
"SECURITY: Server key matches known_hosts - connection verified");
202 log_debug(
"SECURITY_DEBUG: Key mismatch, continuing search...");
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!");
228 log_warn(
"Known hosts file does not exist: %s", path);
237 log_warn(
"Failed to open known hosts file: %s", path);
252 safe_snprintf(expected_prefix,
sizeof(expected_prefix),
"%s ", ip_with_port);
254 while (fgets(line,
sizeof(line), f)) {
258 if (strncmp(line, expected_prefix, strlen(expected_prefix)) == 0) {
263 size_t prefix_len = strlen(expected_prefix);
264 size_t line_len = strlen(line);
265 if (line_len < prefix_len) {
270 char *key_type = line + prefix_len;
272 while (*key_type ==
' ' || *key_type ==
'\t') {
275 if (strncmp(key_type,
"no-identity", 11) == 0) {
283 log_warn(
"Server previously had identity key but now has none - potential security issue");
297 if (!path || !*path) {
302 size_t len = strlen(path);
308 memcpy(tmp, path, len + 1);
314 if (len >= 2 && tmp[1] ==
':') {
319 while (*p ==
'/' || *p ==
'\\') {
325 if (*p ==
'/' || *p ==
'\\') {
349 if (!server_ip || !server_key) {
354 size_t ip_len = strlen(server_ip);
360 if (!path || path[0] ==
'\0') {
366 size_t path_len = strlen(path);
377 memcpy(dir, path, path_len + 1);
413 bool is_placeholder =
true;
418 hex[i * 2] =
"0123456789abcdef"[
byte >> 4];
419 hex[i * 2 + 1] =
"0123456789abcdef"[
byte & 0xf];
421 is_placeholder =
false;
428 if (is_placeholder) {
430 fprintf_result =
safe_fprintf(f,
"%s %s 0000000000000000000000000000000000000000000000000000000000000000 %s\n",
438 if (fprintf_result < 0) {
443 if (fflush(f) != 0) {
447 log_debug(
"KNOWN_HOSTS: Successfully added host to known_hosts file: %s", path);
481 size_t num_lines = 0;
484 for (
size_t i = 0; i < num_lines; i++) {
493 safe_snprintf(expected_prefix,
sizeof(expected_prefix),
"%s ", ip_with_port);
495 while (fgets(line,
sizeof(line), f)) {
497 if (strncmp(line, expected_prefix, strlen(expected_prefix)) != 0) {
499 char **new_lines =
SAFE_REALLOC((
void *)lines, (num_lines + 1) *
sizeof(
char *),
char **);
503 if (lines[num_lines] == NULL) {
526 for (
size_t i = 0; i < num_lines; i++) {
527 (void)fputs(lines[i], f);
531 log_debug(
"KNOWN_HOSTS: Successfully removed host from known_hosts file: %s", path);
543 fingerprint[i * 2] =
"0123456789abcdef"[
byte >> 4];
544 fingerprint[i * 2 + 1] =
"0123456789abcdef"[
byte & 0xf];
558 safe_snprintf(ip_with_port,
sizeof(ip_with_port),
"%s:%u", server_ip, port);
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.");
570 if (env_claudecode && strlen(env_claudecode) > 0) {
571 log_warn(
"Skipping known_hosts checking (CLAUDECODE set in debug build).");
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"
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"
587 "Connection aborted for security.\n"
588 "To bypass this check, set the environment variable ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK to 1",
598 log_plain(
"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
599 "@ WARNING: REMOTE HOST IDENTIFICATION NOT KNOWN! @\n"
600 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\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);
611 log_warn(
"Warning: Permanently added '%s' to the list of known hosts.", ip_with_port);
615 log_warn(
"Connection aborted by user.");
622 const uint8_t received_key[32]) {
633 safe_snprintf(ip_with_port,
sizeof(ip_with_port),
"%s:%u", server_ip, port);
636 char escaped_ip_with_port[128];
637 escape_ascii(ip_with_port,
"[]", escaped_ip_with_port, 128);
639 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
640 "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n"
641 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\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"
647 "The fingerprint for the Ed25519 key sent by the remote host is:\n"
650 "Expected fingerprint:\n"
653 "Please contact your system administrator.\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"
659 "To update the key, run:\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"
669 "Host key verification failed.\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);
684 safe_snprintf(ip_with_port,
sizeof(ip_with_port),
"%s:%u", server_ip, port);
688 "The authenticity of host '%s' can't be established.\n"
689 "The server has no identity key to verify its authenticity.\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"
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"
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"
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"
713 "Connection aborted for security.\n"
720 log_warn(
"Warning: Proceeding with unverified connection.\n"
721 "Your data may be intercepted by attackers!\n"
726 log_plain(
"Connection aborted by user.");
732 if (g_known_hosts_path_cache) {
734 g_known_hosts_path_cache = NULL;
⚠️‼️ 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)
#define SAFE_MALLOC(size, cast)
#define ASCII_CHAT_APP_NAME
Application name for key comments ("ascii-chat")
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.
#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.
#define defer(action)
Defer a cleanup action until function scope exit.
#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)
@ ERROR_CRYPTO_VERIFICATION
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)
#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)
#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.
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
Escape ASCII characters in a string.
🌍 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
Cross-platform interactive prompting utilities.
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities