diff --git a/cmd/hostkeeper/add.go b/cmd/hostkeeper/add.go index 7c40526..8a9056b 100644 --- a/cmd/hostkeeper/add.go +++ b/cmd/hostkeeper/add.go @@ -13,7 +13,6 @@ import ( "git.tukangketik.id/swanadiva/hostkeeper/internal/models" "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" - "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) var ( @@ -155,7 +154,7 @@ func runAddHost(cmd *cobra.Command, args []string) error { } // Initialize storage - store, err := storage.NewJSONStorage(cfg.GetDataDir()) + store, err := newStorage(cfg) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } @@ -278,7 +277,7 @@ func addHostInteractive(cfg *config.Config) error { } // Initialize storage - store, err := storage.NewJSONStorage(cfg.GetDataDir()) + store, err := newStorage(cfg) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } diff --git a/cmd/hostkeeper/delete.go b/cmd/hostkeeper/delete.go index 869fe1d..d694f97 100644 --- a/cmd/hostkeeper/delete.go +++ b/cmd/hostkeeper/delete.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" - "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) var deleteForce bool @@ -49,7 +48,7 @@ func runDeleteHost(cmd *cobra.Command, args []string) error { } } - store, err := storage.NewJSONStorage(cfg.GetDataDir()) + store, err := newStorage(cfg) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } diff --git a/cmd/hostkeeper/edit.go b/cmd/hostkeeper/edit.go index 6fd8fd2..0217a56 100644 --- a/cmd/hostkeeper/edit.go +++ b/cmd/hostkeeper/edit.go @@ -75,7 +75,7 @@ func runEditHost(cmd *cobra.Command, args []string) error { } } - store, err := storage.NewJSONStorage(cfg.GetDataDir()) + store, err := newStorage(cfg) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } diff --git a/cmd/hostkeeper/list.go b/cmd/hostkeeper/list.go index cd377b7..3ea7f2b 100644 --- a/cmd/hostkeeper/list.go +++ b/cmd/hostkeeper/list.go @@ -12,7 +12,6 @@ import ( "git.tukangketik.id/swanadiva/hostkeeper/internal/models" "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" - "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) var ( @@ -69,7 +68,7 @@ func runListHosts(cmd *cobra.Command, args []string) error { } // Initialize storage - store, err := storage.NewJSONStorage(cfg.GetDataDir()) + store, err := newStorage(cfg) if err != nil { return fmt.Errorf("failed to initialize storage: %w", err) } diff --git a/cmd/hostkeeper/root.go b/cmd/hostkeeper/root.go index bfa2e4e..cbe340b 100644 --- a/cmd/hostkeeper/root.go +++ b/cmd/hostkeeper/root.go @@ -7,13 +7,15 @@ import ( "github.com/spf13/viper" "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" + "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) var ( - cfgFile string - appCfg *config.Config - verbose int - debug bool + cfgFile string + appCfg *config.Config + verbose int + debug bool + passwordFlag string ) var rootCmd = &cobra.Command{ @@ -47,6 +49,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is platform-specific app dir)") rootCmd.PersistentFlags().CountVarP(&verbose, "verbose", "v", "verbose output (-v for info, -vv for debug)") rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug mode") + rootCmd.PersistentFlags().StringVar(&passwordFlag, "password", "", "master password for encrypted storage") rootCmd.AddCommand(versionCmd) } @@ -84,4 +87,16 @@ var versionCmd = &cobra.Command{ // Execute runs the root command func Execute() error { return rootCmd.Execute() +} + +// newStorage creates a new JSONStorage with the password flag applied +func newStorage(cfg *config.Config) (*storage.JSONStorage, error) { + store, err := storage.NewJSONStorage(cfg.GetDataDir()) + if err != nil { + return nil, err + } + if passwordFlag != "" { + store.SetPassword(passwordFlag) + } + return store, nil } \ No newline at end of file diff --git a/cmd/hostkeeper/tui.go b/cmd/hostkeeper/tui.go index 6e20ac1..6066563 100644 --- a/cmd/hostkeeper/tui.go +++ b/cmd/hostkeeper/tui.go @@ -49,22 +49,26 @@ func runTUI(cmd *cobra.Command, args []string) error { fmt.Printf("Warning: failed to load known_hosts: %v\n", err) } - ctx := context.Background() - hosts, err := store.ListHosts(ctx) - if err != nil { - return fmt.Errorf("failed to load hosts: %w", err) - } + // Auto-detect encrypted data files + needPassword := encryptFlag || store.IsDataEncrypted() model := tui.New() model.SetDataDir(cfg.GetDataDir()) - model.LoadHosts(hosts) if kh != nil { model.SetKnownHosts(kh) } - // If --encrypt flag, show password prompt first - if encryptFlag { + if needPassword { + // Show password prompt first — hosts will be loaded after password is set model.ShowEncryptPrompt() + } else { + // No encryption — load hosts normally + ctx := context.Background() + hosts, err := store.ListHosts(ctx) + if err != nil { + return fmt.Errorf("failed to load hosts: %w", err) + } + model.LoadHosts(hosts) } p := tea.NewProgram(model) diff --git a/pkg/storage/json_storage.go b/pkg/storage/json_storage.go index 609abcb..829ed9e 100644 --- a/pkg/storage/json_storage.go +++ b/pkg/storage/json_storage.go @@ -52,6 +52,16 @@ func (s *JSONStorage) IsEncrypted() bool { return s.password != "" } +// IsDataEncrypted checks if the data files are actually encrypted +func (s *JSONStorage) IsDataEncrypted() bool { + path := filepath.Join(s.dataDir, "hosts.json") + data, err := os.ReadFile(path) + if err != nil { + return false + } + return crypto.IsEncrypted(string(data)) +} + func (s *JSONStorage) ensureDataFiles() error { files := map[string]string{ "hosts.json": "hosts", diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index ef70d10..33a0378 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -117,9 +117,28 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd case passwordSetMsg: - // Password was set — store it and proceed to host list + // Password was set — store it and load hosts m.storagePassword = msg.password - // Close the password prompt tab, host list should already be there + + // Load hosts with the password + store, err := storage.NewJSONStorage(m.dataDir) + if err == nil { + store.SetPassword(msg.password) + ctx := context.Background() + hosts, loadErr := store.ListHosts(ctx) + if loadErr == nil { + m.Hosts = hosts + // Update host list tab if it exists + if hl, ok := m.tabs.Active().(*HostListTab); ok { + hl.SetHosts(hosts) + } + } + } + + // Close the password prompt tab + if m.tabs.Len() > 1 { + m.tabs.CloseActive() + } return m, nil case openEncryptPromptMsg: