C · 37421 bytes Raw Blame History
1 /* Account management and operations with comprehensive security validation
2 * Implements secure account switching and management for gitswitch-c
3 */
4
5 /* Enable POSIX extensions for setenv/unsetenv */
6 #define _POSIX_C_SOURCE 200809L
7
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 #include <ctype.h>
12 #include <sys/stat.h>
13 #include <unistd.h>
14
15 #include "accounts.h"
16 #include "config.h"
17 #include "display.h"
18 #include "error.h"
19 #include "utils.h"
20 #include "git_ops.h"
21 #include "ssh_manager.h"
22 #include "gpg_manager.h"
23
24 /* Active session state - tracks SSH/GPG resources for proper cleanup */
25 typedef struct {
26 ssh_config_t ssh_config;
27 gpg_config_t gpg_config;
28 bool ssh_active;
29 bool gpg_active;
30 char original_gnupghome[MAX_PATH_LEN];
31 bool had_original_gnupghome;
32 bool gnupghome_saved;
33 } active_session_t;
34
35 /* Static session state - only one active session at a time */
36 static active_session_t g_session = {0};
37
38 /* Internal helper functions */
39 static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx);
40 static int validate_ssh_key_security(const char *ssh_key_path);
41 static int validate_gpg_key_availability(const char *gpg_key_id);
42 static int test_ssh_key_functionality(const account_t *account);
43 static int test_gpg_key_functionality(const account_t *account);
44
45 /* Initialize accounts system */
46 int accounts_init(gitswitch_ctx_t *ctx) {
47 if (!ctx) {
48 set_error(ERR_INVALID_ARGS, "NULL context to accounts_init");
49 return -1;
50 }
51
52 /* Initialize account array */
53 memset(ctx->accounts, 0, sizeof(ctx->accounts));
54 ctx->account_count = 0;
55 ctx->current_account = NULL;
56
57 /* Initialize session state */
58 memset(&g_session, 0, sizeof(g_session));
59
60 log_debug("Accounts system initialized");
61 return 0;
62 }
63
64 /* Clean up active session resources */
65 void accounts_session_cleanup(void) {
66 log_debug("Cleaning up active session resources");
67
68 /* Clean up SSH agent if we started one */
69 if (g_session.ssh_active) {
70 log_info("Stopping SSH agent (pid=%d)", g_session.ssh_config.agent_pid);
71 ssh_manager_cleanup(&g_session.ssh_config);
72 g_session.ssh_active = false;
73 }
74
75 /* Clean up GPG environment if we modified it */
76 if (g_session.gpg_active) {
77 log_info("Cleaning up GPG environment");
78 gpg_manager_cleanup(&g_session.gpg_config);
79 g_session.gpg_active = false;
80 }
81
82 /* Restore original GNUPGHOME environment variable */
83 if (g_session.gnupghome_saved) {
84 if (g_session.had_original_gnupghome) {
85 log_debug("Restoring original GNUPGHOME: %s", g_session.original_gnupghome);
86 setenv("GNUPGHOME", g_session.original_gnupghome, 1);
87 } else {
88 log_debug("Unsetting GNUPGHOME (was not set originally)");
89 unsetenv("GNUPGHOME");
90 }
91 g_session.gnupghome_saved = false;
92 }
93
94 /* Clear session state */
95 memset(&g_session, 0, sizeof(g_session));
96 log_debug("Session cleanup complete");
97 }
98
99 /* Switch to specified account with SSH isolation and validation */
100 int accounts_switch(gitswitch_ctx_t *ctx, const char *identifier) {
101 account_t *account;
102 const char *scope_str;
103 bool ssh_ok = false;
104 bool gpg_ok = false;
105
106 if (!ctx || !identifier) {
107 set_error(ERR_INVALID_ARGS, "Invalid arguments to accounts_switch");
108 return -1;
109 }
110
111 /* Find the account */
112 account = config_find_account(ctx, identifier);
113 if (!account) {
114 set_error(ERR_ACCOUNT_NOT_FOUND, "Account not found: %s", identifier);
115 return -1;
116 }
117
118 /* Basic validation */
119 if (!validate_name(account->name) || !validate_email(account->email)) {
120 set_error(ERR_ACCOUNT_INVALID, "Account has invalid name or email");
121 return -1;
122 }
123
124 /* Clean up any previous session before starting new one */
125 accounts_session_cleanup();
126
127 /* Save original GNUPGHOME if not already saved */
128 if (!g_session.gnupghome_saved) {
129 const char *orig = getenv("GNUPGHOME");
130 if (orig) {
131 safe_strncpy(g_session.original_gnupghome, orig, sizeof(g_session.original_gnupghome));
132 g_session.had_original_gnupghome = true;
133 } else {
134 g_session.had_original_gnupghome = false;
135 }
136 g_session.gnupghome_saved = true;
137 }
138
139 /* Determine git scope - use account preference or context default */
140 git_scope_t scope = account->preferred_scope;
141 if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
142 display_warning("Not in a git repository, using global scope instead of local");
143 scope = GIT_SCOPE_GLOBAL;
144 }
145 scope_str = (scope == GIT_SCOPE_LOCAL) ? "local" : "global";
146
147 /* Initialize git operations if not already done */
148 if (git_ops_init() != 0) {
149 set_error(ERR_GIT_CONFIG_FAILED, "Failed to initialize git operations");
150 return -1;
151 }
152
153 /* Show what we're doing */
154 printf("\nSwitching to account: %s <%s>\n", account->name, account->email);
155
156 /* If not in dry-run mode, actually set git configuration */
157 if (!ctx->config.dry_run) {
158 log_info("Setting git configuration for account: %s (%s scope)",
159 account->name, scope_str);
160
161 if (git_set_config(account, scope) != 0) {
162 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set git configuration: %s",
163 get_last_error()->message);
164 return -1;
165 }
166 printf(" [OK] Git config set (%s scope)\n", scope_str);
167
168 /* Validate the configuration was set correctly */
169 if (git_test_config(account, scope) != 0) {
170 log_warning("Git configuration validation failed: %s", get_last_error()->message);
171 /* Don't fail completely, just warn */
172 }
173
174 /* Handle SSH agent isolation if SSH is enabled */
175 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
176 log_info("Setting up SSH isolation for account: %s", account->name);
177
178 /* Initialize SSH manager with isolated agents using session state */
179 memset(&g_session.ssh_config, 0, sizeof(g_session.ssh_config));
180 if (ssh_manager_init(&g_session.ssh_config, SSH_AGENT_ISOLATED) != 0) {
181 printf(" [!!] SSH agent failed to start\n");
182 log_warning("Failed to initialize SSH manager: %s", get_last_error()->message);
183 } else {
184 /* Switch to account's SSH configuration */
185 if (ssh_switch_account(&g_session.ssh_config, account) != 0) {
186 printf(" [!!] SSH key failed to load\n");
187 log_warning("Failed to switch SSH configuration: %s", get_last_error()->message);
188 /* Clean up SSH manager on failure */
189 ssh_manager_cleanup(&g_session.ssh_config);
190 } else {
191 ssh_ok = true;
192 g_session.ssh_active = true; /* Mark session as active for cleanup */
193 printf(" [OK] SSH key loaded\n");
194 log_info("SSH isolation activated for account: %s", account->name);
195
196 /* Test SSH connection if connection testing is available */
197 if (strlen(account->ssh_host_alias) > 0) {
198 if (ssh_test_connection(account, account->ssh_host_alias) == 0) {
199 printf(" [OK] SSH connection verified (%s)\n", account->ssh_host_alias);
200 } else {
201 printf(" [--] SSH connection test skipped (%s unreachable)\n", account->ssh_host_alias);
202 }
203 } else {
204 /* Test with default GitHub host (git@ is required for GitHub SSH) */
205 if (ssh_test_connection(account, "git@github.com") == 0) {
206 printf(" [OK] SSH connection verified (github.com)\n");
207 }
208 /* Silently skip if GitHub unreachable - not an error */
209 }
210 }
211 }
212 }
213
214 /* Handle GPG environment isolation if GPG is enabled */
215 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
216 log_info("Setting up GPG isolation for account: %s", account->name);
217
218 /* Initialize GPG manager with isolated environments using session state */
219 memset(&g_session.gpg_config, 0, sizeof(g_session.gpg_config));
220 if (gpg_manager_init(&g_session.gpg_config, GPG_MODE_ISOLATED) != 0) {
221 printf(" [!!] GPG manager failed to initialize\n");
222 log_warning("Failed to initialize GPG manager: %s", get_last_error()->message);
223 } else {
224 /* Switch to account's GPG configuration */
225 if (gpg_switch_account(&g_session.gpg_config, account) != 0) {
226 printf(" [!!] GPG key failed to activate\n");
227 log_warning("Failed to switch GPG configuration: %s", get_last_error()->message);
228 /* Clean up GPG manager on failure */
229 gpg_manager_cleanup(&g_session.gpg_config);
230 } else {
231 g_session.gpg_active = true; /* Mark session as active for cleanup */
232 log_info("GPG isolation activated for account: %s", account->name);
233
234 /* Configure git GPG signing */
235 if (gpg_configure_git_signing(&g_session.gpg_config, account, scope) != 0) {
236 printf(" [!!] GPG signing config failed\n");
237 log_warning("Failed to configure git GPG signing: %s", get_last_error()->message);
238 } else {
239 gpg_ok = true;
240 printf(" [OK] GPG signing enabled (key: %s)\n", account->gpg_key_id);
241 log_info("Git GPG signing configured for account: %s", account->name);
242 }
243 }
244 }
245 }
246 } else {
247 printf(" [--] DRY RUN: Would set git config (%s scope)\n", scope_str);
248 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
249 printf(" [--] DRY RUN: Would load SSH key\n");
250 }
251 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
252 printf(" [--] DRY RUN: Would enable GPG signing\n");
253 }
254 }
255
256 /* Test SSH functionality if enabled (basic validation) */
257 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0 && !ssh_ok) {
258 if (test_ssh_key_functionality(account) != 0) {
259 log_warning("SSH key test failed for account: %s", account->name);
260 }
261 }
262
263 /* Test GPG functionality if enabled */
264 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0 && !gpg_ok) {
265 if (test_gpg_key_functionality(account) != 0) {
266 log_warning("GPG key test failed for account: %s", account->name);
267 }
268 }
269
270 /* Set as current account */
271 ctx->current_account = account;
272
273 /* Print shell integration tip if SSH was set up */
274 if (ssh_ok) {
275 const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
276 if (runtime_dir) {
277 printf("\n Tip: Add to your shell rc for persistent SSH:\n");
278 printf(" bash/zsh: export SSH_AUTH_SOCK=%s/gitswitch-ssh/current.sock\n", runtime_dir);
279 printf(" fish: set -gx SSH_AUTH_SOCK %s/gitswitch-ssh/current.sock\n", runtime_dir);
280 }
281 }
282
283 printf("\n");
284 log_info("Successfully switched to account: %s (%s)", account->name, account->description);
285 return 0;
286 }
287
288 /* Add new account interactively with basic validation */
289 int accounts_add_interactive(gitswitch_ctx_t *ctx) {
290 account_t new_account;
291 char input[512];
292 char expanded_path[MAX_PATH_LEN];
293
294 if (!ctx) {
295 set_error(ERR_INVALID_ARGS, "NULL context to accounts_add_interactive");
296 return -1;
297 }
298
299 if (ctx->account_count >= MAX_ACCOUNTS) {
300 set_error(ERR_ACCOUNT_EXISTS, "Maximum number of accounts reached: %d", MAX_ACCOUNTS);
301 return -1;
302 }
303
304 /* Initialize new account */
305 memset(&new_account, 0, sizeof(new_account));
306 new_account.id = get_next_available_id(ctx);
307 new_account.preferred_scope = ctx->config.default_scope;
308
309 printf("\n┌─────────────────────────────────────┐\n");
310 printf("│ Add New Account │\n");
311 printf("└─────────────────────────────────────┘\n\n");
312
313 /* Get account name */
314 do {
315 printf("Account Name: ");
316 fflush(stdout);
317
318 if (!fgets(input, sizeof(input), stdin)) {
319 set_error(ERR_FILE_IO, "Failed to read account name");
320 return -1;
321 }
322
323 input[strcspn(input, "\n")] = '\0';
324 trim_whitespace(input);
325
326 if (!validate_name(input)) {
327 printf("[ERROR]: Invalid name. Please enter a non-empty name.\n");
328 continue;
329 }
330
331 safe_strncpy(new_account.name, input, sizeof(new_account.name));
332 break;
333 } while (1);
334
335 /* Get email address */
336 do {
337 printf("Email Address: ");
338 fflush(stdout);
339
340 if (!fgets(input, sizeof(input), stdin)) {
341 set_error(ERR_FILE_IO, "Failed to read email address");
342 return -1;
343 }
344
345 input[strcspn(input, "\n")] = '\0';
346 trim_whitespace(input);
347
348 if (!validate_email(input)) {
349 printf("[ERROR]: Invalid email address format.\n");
350 continue;
351 }
352
353 safe_strncpy(new_account.email, input, sizeof(new_account.email));
354 break;
355 } while (1);
356
357 /* Get description */
358 printf("Description (optional): ");
359 fflush(stdout);
360
361 if (fgets(input, sizeof(input), stdin)) {
362 input[strcspn(input, "\n")] = '\0';
363 trim_whitespace(input);
364
365 if (strlen(input) > 0) {
366 safe_strncpy(new_account.description, input, sizeof(new_account.description));
367 } else {
368 safe_strncpy(new_account.description, new_account.name, sizeof(new_account.description));
369 }
370 } else {
371 safe_strncpy(new_account.description, new_account.name, sizeof(new_account.description));
372 }
373
374 /* Get SSH key configuration */
375 printf("SSH Key Path (optional, press Enter to skip): ");
376 fflush(stdout);
377
378 if (fgets(input, sizeof(input), stdin)) {
379 input[strcspn(input, "\n")] = '\0';
380 trim_whitespace(input);
381
382 if (strlen(input) > 0) {
383 /* Expand and validate path */
384 if (expand_path(input, expanded_path, sizeof(expanded_path)) == 0) {
385 if (path_exists(expanded_path)) {
386 if (validate_ssh_key_security(expanded_path) == 0) {
387 safe_strncpy(new_account.ssh_key_path, expanded_path, sizeof(new_account.ssh_key_path));
388 new_account.ssh_enabled = true;
389 printf("[OK]: SSH key validated: %s\n", expanded_path);
390
391 /* Optional SSH host alias */
392 printf("SSH Host Alias (optional, e.g., github.com-work): ");
393 fflush(stdout);
394
395 if (fgets(input, sizeof(input), stdin)) {
396 input[strcspn(input, "\n")] = '\0';
397 trim_whitespace(input);
398
399 if (strlen(input) > 0) {
400 safe_strncpy(new_account.ssh_host_alias, input, sizeof(new_account.ssh_host_alias));
401 }
402 }
403 } else {
404 printf("[ERROR]: SSH key validation failed. Continuing without SSH key.\n");
405 }
406 } else {
407 printf("[ERROR]: SSH key file not found: %s\n", expanded_path);
408 }
409 } else {
410 printf("[ERROR]: Invalid SSH key path: %s\n", input);
411 }
412 }
413 }
414
415 /* Get GPG key configuration */
416 printf("GPG Key ID (optional, press Enter to skip): ");
417 fflush(stdout);
418
419 if (fgets(input, sizeof(input), stdin)) {
420 input[strcspn(input, "\n")] = '\0';
421 trim_whitespace(input);
422
423 if (strlen(input) > 0) {
424 if (validate_key_id(input)) {
425 if (validate_gpg_key_availability(input) == 0) {
426 safe_strncpy(new_account.gpg_key_id, input, sizeof(new_account.gpg_key_id));
427 new_account.gpg_enabled = true;
428 printf("[OK]: GPG key validated: %s\n", input);
429
430 /* Ask about GPG signing */
431 printf("Enable GPG signing for commits? (y/N): ");
432 fflush(stdout);
433
434 if (fgets(input, sizeof(input), stdin)) {
435 input[strcspn(input, "\n")] = '\0';
436 trim_whitespace(input);
437
438 new_account.gpg_signing_enabled = (tolower(input[0]) == 'y');
439 }
440 } else {
441 printf("[ERROR]: GPG key validation failed. Continuing without GPG key.\n");
442 }
443 } else {
444 printf("[ERROR]: Invalid GPG key ID format: %s\n", input);
445 }
446 }
447 }
448
449 /* Get preferred scope */
450 printf("Preferred Git Scope (local/global) [%s]: ",
451 config_scope_to_string(new_account.preferred_scope));
452 fflush(stdout);
453
454 if (fgets(input, sizeof(input), stdin)) {
455 input[strcspn(input, "\n")] = '\0';
456 trim_whitespace(input);
457
458 if (strlen(input) > 0) {
459 new_account.preferred_scope = config_parse_scope(input);
460 }
461 }
462
463 /* Basic validation */
464 if (!validate_name(new_account.name) || !validate_email(new_account.email)) {
465 printf("[ERROR]: Account validation failed: Invalid name or email\n");
466 return -1;
467 }
468
469 /* Confirmation */
470 printf("\nAccount Summary:\n");
471 printf(" ID: %u\n", new_account.id);
472 printf(" Name: %s\n", new_account.name);
473 printf(" Email: %s\n", new_account.email);
474 printf(" Description: %s\n", new_account.description);
475 printf(" Scope: %s\n", config_scope_to_string(new_account.preferred_scope));
476 printf(" SSH: %s\n", new_account.ssh_enabled ? "[ENABLED]" : "[DISABLED]");
477 printf(" GPG: %s\n", new_account.gpg_enabled ? "[ENABLED]" : "[DISABLED]");
478
479 printf("\nAdd this account? (y/N): ");
480 fflush(stdout);
481
482 if (!fgets(input, sizeof(input), stdin)) {
483 set_error(ERR_FILE_IO, "Failed to read confirmation");
484 return -1;
485 }
486
487 input[strcspn(input, "\n")] = '\0';
488 trim_whitespace(input);
489
490 if (tolower(input[0]) != 'y') {
491 printf("Account creation cancelled.\n");
492 return -1;
493 }
494
495 /* Add account to context */
496 if (config_add_account(ctx, &new_account) != 0) {
497 return -1;
498 }
499
500 printf("[OK]: Account added successfully!\n");
501 return 0;
502 }
503
504 /* Remove account with confirmation and cleanup */
505 int accounts_remove(gitswitch_ctx_t *ctx, const char *identifier) {
506 account_t *account;
507 char input[64];
508
509 if (!ctx || !identifier) {
510 set_error(ERR_INVALID_ARGS, "Invalid arguments to accounts_remove");
511 return -1;
512 }
513
514 /* Find the account */
515 account = config_find_account(ctx, identifier);
516 if (!account) {
517 set_error(ERR_ACCOUNT_NOT_FOUND, "Account not found: %s", identifier);
518 return -1;
519 }
520
521 /* Show account details */
522 printf("\nRemove Account\n");
523 printf("─────────────────\n");
524 printf("ID: %u\n", account->id);
525 printf("Name: %s\n", account->name);
526 printf("Email: %s\n", account->email);
527 printf("Description: %s\n", account->description);
528
529 /* Confirmation */
530 printf("\n[WARN]: This will permanently remove the account from configuration.\n");
531 printf("Are you sure? (type 'yes' to confirm): ");
532 fflush(stdout);
533
534 if (!fgets(input, sizeof(input), stdin)) {
535 set_error(ERR_FILE_IO, "Failed to read confirmation");
536 return -1;
537 }
538
539 input[strcspn(input, "\n")] = '\0';
540 trim_whitespace(input);
541
542 if (strcmp(input, "yes") != 0) {
543 printf("Account removal cancelled.\n");
544 return 0;
545 }
546
547 /* Clear current account if it's the one being removed */
548 if (ctx->current_account == account) {
549 ctx->current_account = NULL;
550 }
551
552 uint32_t account_id = account->id;
553
554 /* Remove account */
555 if (config_remove_account(ctx, account_id) != 0) {
556 return -1;
557 }
558
559 printf("[OK]: Account removed successfully.\n");
560 return 0;
561 }
562
563 /* List all configured accounts */
564 int accounts_list(const gitswitch_ctx_t *ctx) {
565 if (!ctx) {
566 set_error(ERR_INVALID_ARGS, "NULL context to accounts_list");
567 return -1;
568 }
569
570 if (ctx->account_count == 0) {
571 printf("\n[INFO]: No accounts configured.\n");
572 printf("Run 'gitswitch add' to create your first account.\n\n");
573 return 0;
574 }
575
576 printf("\nConfigured Accounts (%zu total)\n", ctx->account_count);
577 printf("════════════════════════════════════════════════════════════════\n");
578
579 for (size_t i = 0; i < ctx->account_count; i++) {
580 const account_t *account = &ctx->accounts[i];
581 bool is_current = (ctx->current_account == account);
582
583 printf("%s [%u] %s\n", is_current ? "[CURRENT]" : "", account->id, account->name);
584 printf(" Email: %s\n", account->email);
585 printf(" Description: %s\n", account->description);
586 printf(" Scope: %s\n", config_scope_to_string(account->preferred_scope));
587
588 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
589 printf(" SSH Key: %s\n", account->ssh_key_path);
590 if (strlen(account->ssh_host_alias) > 0) {
591 printf(" Host: %s\n", account->ssh_host_alias);
592 }
593 } else {
594 printf(" SSH Key: Not configured\n");
595 }
596
597 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
598 printf(" GPG Key: %s %s\n", account->gpg_key_id,
599 account->gpg_signing_enabled ? "(signing enabled)" : "(signing disabled)");
600 } else {
601 printf(" GPG Key: Not configured\n");
602 }
603
604 if (i < ctx->account_count - 1) {
605 printf("\n");
606 }
607 }
608
609 printf("════════════════════════════════════════════════════════════════\n\n");
610
611 if (ctx->current_account) {
612 printf("Current: %s (%s)\n\n", ctx->current_account->name, ctx->current_account->description);
613 } else {
614 printf("No account currently active.\n\n");
615 }
616
617 return 0;
618 }
619
620 /* Show current account status */
621 int accounts_show_status(const gitswitch_ctx_t *ctx) {
622 if (!ctx) {
623 set_error(ERR_INVALID_ARGS, "NULL context to accounts_show_status");
624 return -1;
625 }
626
627 printf("\nAccount Status\n");
628 printf("════════════════\n");
629
630 if (ctx->current_account) {
631 const account_t *account = ctx->current_account;
632
633 printf("Active Account: %s (ID: %u)\n", account->name, account->id);
634 printf("Email: %s\n", account->email);
635 printf("Description: %s\n", account->description);
636 printf("Preferred Scope: %s\n", config_scope_to_string(account->preferred_scope));
637
638 /* SSH Status */
639 printf("\nSSH Configuration:\n");
640 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
641 printf(" Status: [ENABLED]\n");
642 printf(" Key: %s\n", account->ssh_key_path);
643
644 if (path_exists(account->ssh_key_path)) {
645 printf(" Key File: [FOUND]\n");
646
647 mode_t key_mode;
648 if (get_file_permissions(account->ssh_key_path, &key_mode) == 0) {
649 if ((key_mode & 077) == 0) {
650 printf(" Permissions: [SECURE] (600)\n");
651 } else {
652 printf(" Permissions: [WARN] Insecure (%o)\n", key_mode & 0777);
653 }
654 }
655 } else {
656 printf(" Key File: [NOT FOUND]\n");
657 }
658
659 if (strlen(account->ssh_host_alias) > 0) {
660 printf(" Host Alias: %s\n", account->ssh_host_alias);
661 }
662 } else {
663 printf(" Status: [DISABLED]\n");
664 }
665
666 /* GPG Status */
667 printf("\nGPG Configuration:\n");
668 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
669 printf(" Status: [ENABLED]\n");
670 printf(" Key ID: %s\n", account->gpg_key_id);
671 printf(" Signing: %s\n", account->gpg_signing_enabled ? "[ENABLED]" : "[DISABLED]");
672 } else {
673 printf(" Status: [DISABLED]\n");
674 }
675
676 /* Git Configuration Status */
677 printf("\nGit Configuration:\n");
678 git_current_config_t git_config;
679 if (git_get_current_config(&git_config) == 0) {
680 printf(" Current Name: %s\n", git_config.name);
681 printf(" Current Email: %s\n", git_config.email);
682 printf(" Configuration Scope: %s\n",
683 git_config.scope == GIT_SCOPE_LOCAL ? "local" :
684 git_config.scope == GIT_SCOPE_GLOBAL ? "global" : "system");
685
686 /* Check if git config matches account */
687 if (strcmp(git_config.name, account->name) == 0 &&
688 strcmp(git_config.email, account->email) == 0) {
689 printf(" Match Status: [OK] Git config matches account\n");
690 } else {
691 printf(" Match Status: [WARN] Git config does not match account\n");
692 printf(" Expected: %s <%s>\n", account->name, account->email);
693 printf(" Current: %s <%s>\n", git_config.name, git_config.email);
694 }
695
696 /* GPG signing status */
697 if (strlen(git_config.signing_key) > 0) {
698 printf(" GPG Signing Key: %s\n", git_config.signing_key);
699 printf(" GPG Signing Enabled: %s\n", git_config.gpg_signing_enabled ? "[YES]" : "[NO]");
700 } else {
701 printf(" GPG Signing: [NOT CONFIGURED]\n");
702 }
703 } else {
704 printf(" Status: [NOT FOUND] No git configuration found\n");
705 }
706
707 /* Repository context */
708 printf("\nRepository Context:\n");
709 if (git_is_repository()) {
710 char repo_root[MAX_PATH_LEN];
711 if (git_get_repo_root(repo_root, sizeof(repo_root)) == 0) {
712 printf(" Repository: [FOUND] %s\n", repo_root);
713 } else {
714 printf(" Repository: [REPOSITORY] Current directory is a git repository\n");
715 }
716 } else {
717 printf(" Repository: [NO REPOSITORY] Not in a git repository\n");
718 }
719
720 } else {
721 printf("No account currently active.\n");
722 printf("Run 'gitswitch list' to see available accounts.\n");
723 printf("Run 'gitswitch <account>' to activate an account.\n");
724
725 /* Show current git config even without active account */
726 printf("\nCurrent Git Configuration:\n");
727 git_current_config_t git_config;
728 if (git_get_current_config(&git_config) == 0) {
729 printf(" Name: %s\n", git_config.name);
730 printf(" Email: %s\n", git_config.email);
731 printf(" Scope: %s\n",
732 git_config.scope == GIT_SCOPE_LOCAL ? "local" :
733 git_config.scope == GIT_SCOPE_GLOBAL ? "global" : "system");
734 } else {
735 printf(" Status: [NOT FOUND] No git configuration found\n");
736 }
737
738 /* Repository context */
739 printf("\nRepository Context:\n");
740 if (git_is_repository()) {
741 printf(" Repository: [REPOSITORY] Current directory is a git repository\n");
742 } else {
743 printf(" Repository: [NO REPOSITORY] Not in a git repository\n");
744 }
745 }
746
747 printf("\n");
748 return 0;
749 }
750
751 /* Simple account validation for Phase 2 */
752 int accounts_validate(const account_t *account) {
753 if (!account) {
754 set_error(ERR_INVALID_ARGS, "NULL account pointer");
755 return -1;
756 }
757
758 /* Validate required fields */
759 if (!validate_name(account->name)) {
760 set_error(ERR_ACCOUNT_INVALID, "Invalid or empty account name");
761 return -1;
762 }
763
764 if (!validate_email(account->email)) {
765 set_error(ERR_ACCOUNT_INVALID, "Invalid email address format");
766 return -1;
767 }
768
769 /* Basic SSH validation if enabled */
770 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
771 char expanded_path[MAX_PATH_LEN];
772
773 if (expand_path(account->ssh_key_path, expanded_path, sizeof(expanded_path)) != 0) {
774 set_error(ERR_ACCOUNT_INVALID, "Invalid SSH key path: %s", account->ssh_key_path);
775 return -1;
776 }
777
778 if (!path_exists(expanded_path)) {
779 set_error(ERR_ACCOUNT_INVALID, "SSH key file not found: %s", expanded_path);
780 return -1;
781 }
782 }
783
784 /* Basic GPG validation if enabled */
785 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
786 if (!validate_key_id(account->gpg_key_id)) {
787 set_error(ERR_ACCOUNT_INVALID, "Invalid GPG key ID format: %s", account->gpg_key_id);
788 return -1;
789 }
790 }
791
792 return 0;
793 }
794
795 /* Get next available account ID */
796 static uint32_t get_next_available_id(const gitswitch_ctx_t *ctx) {
797 uint32_t max_id = 0;
798
799 if (!ctx) return 1;
800
801 /* Find the highest existing ID */
802 for (size_t i = 0; i < ctx->account_count; i++) {
803 if (ctx->accounts[i].id > max_id) {
804 max_id = ctx->accounts[i].id;
805 }
806 }
807
808 return max_id + 1;
809 }
810
811 /* Validate SSH key security */
812 static int validate_ssh_key_security(const char *ssh_key_path) {
813 FILE *key_file;
814 char first_line[256];
815 mode_t file_mode;
816
817 if (!ssh_key_path || !path_exists(ssh_key_path)) {
818 return -1;
819 }
820
821 /* Check file permissions */
822 if (get_file_permissions(ssh_key_path, &file_mode) != 0) {
823 return -1;
824 }
825
826 /* Check if it's a private key (should be 600) or public key (can be 644) */
827 if (strstr(ssh_key_path, ".pub") == NULL) {
828 /* Private key - should be 600 */
829 if ((file_mode & 077) != 0) {
830 log_warning("SSH private key file has insecure permissions: %o", file_mode & 0777);
831 return -1;
832 }
833 } else {
834 /* Public key - 644 is acceptable */
835 if ((file_mode & 022) != 0 && (file_mode & 044) == 0) {
836 log_warning("SSH public key file has unusual permissions: %o", file_mode & 0777);
837 /* Continue anyway for public keys */
838 }
839 }
840
841 /* Check if it looks like a valid SSH key */
842 key_file = fopen(ssh_key_path, "r");
843 if (!key_file) {
844 return -1;
845 }
846
847 if (fgets(first_line, sizeof(first_line), key_file)) {
848 /* Check for SSH key formats - both public and private */
849 bool is_valid_key = false;
850
851 /* Private key formats */
852 if (string_starts_with(first_line, "-----BEGIN OPENSSH PRIVATE KEY-----") ||
853 string_starts_with(first_line, "-----BEGIN RSA PRIVATE KEY-----") ||
854 string_starts_with(first_line, "-----BEGIN DSA PRIVATE KEY-----") ||
855 string_starts_with(first_line, "-----BEGIN EC PRIVATE KEY-----") ||
856 string_starts_with(first_line, "-----BEGIN SSH2 PRIVATE KEY-----")) {
857 is_valid_key = true;
858 }
859
860 /* Public key formats */
861 if (string_starts_with(first_line, "ssh-rsa ") ||
862 string_starts_with(first_line, "ssh-dss ") ||
863 string_starts_with(first_line, "ssh-ed25519 ") ||
864 string_starts_with(first_line, "ecdsa-sha2-") ||
865 string_starts_with(first_line, "ssh-ecdsa ")) {
866 is_valid_key = true;
867 }
868
869 if (!is_valid_key) {
870 fclose(key_file);
871 log_warning("SSH key file format not recognized");
872 return -1;
873 }
874 }
875
876 fclose(key_file);
877 return 0;
878 }
879
880 /* Validate GPG key availability */
881 static int validate_gpg_key_availability(const char *gpg_key_id) {
882 char command[256];
883 int result;
884
885 if (!gpg_key_id) {
886 return -1;
887 }
888
889 /* Try to find the key in the GPG keyring */
890 if ((size_t)snprintf(command, sizeof(command), "gpg --list-secret-keys %s >/dev/null 2>&1",
891 gpg_key_id) >= sizeof(command)) {
892 log_error("GPG command too long");
893 return -1;
894 }
895
896 result = system(command);
897 if (result != 0) {
898 log_debug("GPG key %s not found in keyring", gpg_key_id);
899 return -1;
900 }
901
902 return 0;
903 }
904
905 /* Test SSH key functionality */
906 static int test_ssh_key_functionality(const account_t *account) {
907 /* This is a placeholder for SSH functionality testing
908 * In a full implementation, this would:
909 * 1. Start SSH agent if needed
910 * 2. Load the key into agent
911 * 3. Test connection to a known host
912 * 4. Verify authentication works
913 */
914 log_debug("SSH key functionality test for %s: %s",
915 account->name, account->ssh_key_path);
916
917 /* For now, just validate the key file exists and has correct permissions */
918 return validate_ssh_key_security(account->ssh_key_path);
919 }
920
921 /* Test GPG key functionality */
922 static int test_gpg_key_functionality(const account_t *account) {
923 /* This is a placeholder for GPG functionality testing
924 * In a full implementation, this would:
925 * 1. Set up GPG environment
926 * 2. Test key can be used for signing
927 * 3. Verify key is not expired
928 * 4. Test signing a test message
929 */
930 log_debug("GPG key functionality test for %s: %s",
931 account->name, account->gpg_key_id);
932
933 /* For now, just check if key exists in keyring */
934 return validate_gpg_key_availability(account->gpg_key_id);
935 }
936
937 /* Run comprehensive health check on all accounts */
938 int accounts_health_check(const gitswitch_ctx_t *ctx) {
939 bool all_healthy = true;
940
941 if (!ctx) {
942 set_error(ERR_INVALID_ARGS, "NULL context to accounts_health_check");
943 return -1;
944 }
945
946 printf("\nAccount Health Check\n");
947 printf("══════════════════════\n");
948
949 if (ctx->account_count == 0) {
950 printf("[ERROR]: No accounts configured\n");
951 printf(" Run 'gitswitch add' to create your first account\n\n");
952 return -1;
953 }
954
955 for (size_t i = 0; i < ctx->account_count; i++) {
956 const account_t *account = &ctx->accounts[i];
957 int validation_result = accounts_validate(account);
958
959 printf("\n[%u] %s\n", account->id, account->name);
960 printf("────────────────────────\n");
961
962 if (validation_result == 0) {
963 printf("[OK]: Account configuration valid\n");
964
965 /* Test SSH if configured */
966 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
967 if (test_ssh_key_functionality(account) == 0) {
968 printf("[OK]: SSH key functional\n");
969 } else {
970 printf("[ERROR]: SSH key issues detected\n");
971 all_healthy = false;
972 }
973 }
974
975 /* Test GPG if configured */
976 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
977 if (test_gpg_key_functionality(account) == 0) {
978 printf("[OK]: GPG key functional\n");
979 } else {
980 printf("[ERROR]: GPG key issues detected\n");
981 all_healthy = false;
982 }
983 }
984 } else {
985 printf("[ERROR]: Account validation failed\n");
986 all_healthy = false;
987 }
988 }
989
990 printf("\n══════════════════════\n");
991 if (all_healthy) {
992 printf("[OK]: All accounts are healthy\n\n");
993 return 0;
994 } else {
995 printf("[ERROR]: Some accounts have issues\n\n");
996 return -1;
997 }
998 }