package cmd import ( "fmt" "os" "strings" "syscall" "github.com/mirivlad/sshkeeper/internal/config" "github.com/mirivlad/sshkeeper/internal/vault" "github.com/spf13/cobra" "golang.org/x/term" ) var vaultInstance *vault.Vault func getOrCreateVault() *vault.Vault { if vaultInstance == nil { vaultInstance = vault.New(config.VaultPath(cfg.DataDir)) } return vaultInstance } var vaultCmd = &cobra.Command{ Use: "vault", Short: "Vault management commands", } var vaultUnlockCmd = &cobra.Command{ Use: "unlock", Short: "Verify the vault master password", RunE: func(cmd *cobra.Command, args []string) error { v := getOrCreateVault() if v.IsUnlocked() { fmt.Println("Vault is already unlocked.") return nil } vaultPath := config.VaultPath(cfg.DataDir) // Check if vault exists and has content info, err := os.Stat(vaultPath) if os.IsNotExist(err) || info.Size() == 0 { // New vault - create with master password fmt.Print("Create master password: ") pw1, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { return fmt.Errorf("read password: %w", err) } if len(pw1) == 0 { return fmt.Errorf("password cannot be empty") } fmt.Print("Repeat master password: ") pw2, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { return fmt.Errorf("read password: %w", err) } if string(pw1) != string(pw2) { return fmt.Errorf("passwords do not match") } if err := vault.Create(vaultPath, string(pw1)); err != nil { return fmt.Errorf("create vault: %w", err) } if err := v.Unlock(string(pw1)); err != nil { return fmt.Errorf("unlock vault: %w", err) } fmt.Println("Vault created. Commands will ask for the master password when they need secrets.") return nil } fmt.Print("Master password: ") pw, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { return fmt.Errorf("read password: %w", err) } if err := v.Unlock(string(pw)); err != nil { return fmt.Errorf("unlock vault: %w", err) } fmt.Println("Master password accepted. Vault unlock is process-local; commands will ask again when they need secrets.") return nil }, } var vaultLockCmd = &cobra.Command{ Use: "lock", Short: "Lock the vault", RunE: func(cmd *cobra.Command, args []string) error { v := getOrCreateVault() v.Lock() fmt.Println("Vault locked.") return nil }, } var vaultStatusCmd = &cobra.Command{ Use: "status", Short: "Show vault status", RunE: func(cmd *cobra.Command, args []string) error { v := getOrCreateVault() fmt.Println(formatVaultStatus(v.IsUnlocked(), vault.Exists(config.VaultPath(cfg.DataDir)))) return nil }, } var vaultChangePasswordCmd = &cobra.Command{ Use: "change-password", Short: "Change master password", RunE: func(cmd *cobra.Command, args []string) error { v := getOrCreateVault() if err := unlockVaultForCommand(v); err != nil { return err } fmt.Print("New master password: ") pw1, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { return fmt.Errorf("read password: %w", err) } if len(pw1) == 0 { return fmt.Errorf("password cannot be empty") } fmt.Print("Repeat new master password: ") pw2, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { return fmt.Errorf("read password: %w", err) } if string(pw1) != string(pw2) { return fmt.Errorf("passwords do not match") } if err := v.ChangePassword(string(pw1)); err != nil { return fmt.Errorf("change password: %w", err) } fmt.Println("Master password changed.") return nil }, } var vaultListCmd = &cobra.Command{ Use: "list", Short: "List stored secret metadata", RunE: func(cmd *cobra.Command, args []string) error { v := getOrCreateVault() if err := unlockVaultForCommand(v); err != nil { return err } output, err := formatVaultSecretsList(v) if err != nil { return err } fmt.Print(output) return nil }, } var vaultDeleteCmd = &cobra.Command{ Use: "delete [type]", Short: "Delete stored secrets for a server", Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { alias := args[0] secretType := "" if len(args) == 2 { secretType = args[1] } v := getOrCreateVault() if err := unlockVaultForCommand(v); err != nil { return err } if err := deleteVaultSecrets(v, alias, secretType); err != nil { return err } if err := v.Save(); err != nil { return fmt.Errorf("save vault: %w", err) } if secretType == "" { fmt.Printf("Deleted secrets for %s.\n", alias) } else { fmt.Printf("Deleted %s for %s.\n", secretType, alias) } return nil }, } func unlockVaultForCommand(v *vault.Vault) error { if v.IsUnlocked() { return nil } fmt.Print("Master password: ") pw, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { return fmt.Errorf("read password: %w", err) } if err := v.Unlock(string(pw)); err != nil { return fmt.Errorf("unlock vault: %w", err) } return nil } func vaultLockedProcessMessage() string { return "vault is locked in this process; enter the master password when this command prompts for it" } func formatVaultStatus(unlocked bool, exists bool) string { if !exists { return "Vault: not found" } if unlocked { return "Vault: unlocked in current process" } return "Vault: locked (vault commands unlock per command)" } func formatVaultSecretsList(v *vault.Vault) (string, error) { metas, err := v.ListSecrets() if err != nil { return "", err } if len(metas) == 0 { return "No secrets stored.\n", nil } var b strings.Builder fmt.Fprintf(&b, "%-24s %-18s\n", "ALIAS", "TYPE") for _, meta := range metas { fmt.Fprintf(&b, "%-24s %-18s\n", meta.Alias, meta.Type) } return b.String(), nil } func init() { vaultCmd.AddCommand(vaultUnlockCmd) vaultCmd.AddCommand(vaultLockCmd) vaultCmd.AddCommand(vaultStatusCmd) vaultCmd.AddCommand(vaultChangePasswordCmd) vaultCmd.AddCommand(vaultListCmd) vaultCmd.AddCommand(vaultDeleteCmd) }