feat: auto-detect encrypted files + CLI --password flag

- Storage.IsDataEncrypted() checks if hosts.json is encrypted
- TUI auto-detects encrypted files, prompts password automatically
- Root command: --password flag for all CLI commands
- newStorage() helper applies password flag to storage
- add/list/edit/delete commands now support encrypted storage
- passwordSetMsg loads hosts after password is set
This commit is contained in:
swanadiva
2026-06-25 14:34:41 +07:00
parent 94f2142d3d
commit 93957e3989
8 changed files with 67 additions and 22 deletions
+2 -3
View File
@@ -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)
}
+1 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
+1 -2
View File
@@ -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)
}
+15
View File
@@ -7,6 +7,7 @@ import (
"github.com/spf13/viper"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
)
var (
@@ -14,6 +15,7 @@ var (
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)
}
@@ -85,3 +88,15 @@ var versionCmd = &cobra.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
}
+14 -10
View File
@@ -49,22 +49,26 @@ func runTUI(cmd *cobra.Command, args []string) error {
fmt.Printf("Warning: failed to load known_hosts: %v\n", err)
}
// Auto-detect encrypted data files
needPassword := encryptFlag || store.IsDataEncrypted()
model := tui.New()
model.SetDataDir(cfg.GetDataDir())
if kh != nil {
model.SetKnownHosts(kh)
}
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 := tui.New()
model.SetDataDir(cfg.GetDataDir())
model.LoadHosts(hosts)
if kh != nil {
model.SetKnownHosts(kh)
}
// If --encrypt flag, show password prompt first
if encryptFlag {
model.ShowEncryptPrompt()
}
p := tea.NewProgram(model)
+10
View File
@@ -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",
+21 -2
View File
@@ -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: