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