diff --git a/cmd/hostkeeper/tui.go b/cmd/hostkeeper/tui.go index 47824cc..a060227 100644 --- a/cmd/hostkeeper/tui.go +++ b/cmd/hostkeeper/tui.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" + "git.tukangketik.id/swanadiva/hostkeeper/pkg/knownhosts" "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" "git.tukangketik.id/swanadiva/hostkeeper/pkg/tui" ) @@ -39,6 +40,12 @@ func runTUI(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to initialize storage: %w", err) } + // Initialize known_hosts + kh, err := knownhosts.New(cfg.GetDataDir()) + if err != nil { + fmt.Printf("Warning: failed to load known_hosts: %v\n", err) + } + ctx := context.Background() hosts, err := store.ListHosts(ctx) if err != nil { @@ -48,6 +55,9 @@ func runTUI(cmd *cobra.Command, args []string) error { model := tui.New() model.SetDataDir(cfg.GetDataDir()) model.LoadHosts(hosts) + if kh != nil { + model.SetKnownHosts(kh) + } p := tea.NewProgram(model) model.SetProgram(p) diff --git a/pkg/tui/sftp_browser_tab.go b/pkg/tui/sftp_browser_tab.go index 4faf7ff..1ec42c7 100644 --- a/pkg/tui/sftp_browser_tab.go +++ b/pkg/tui/sftp_browser_tab.go @@ -74,6 +74,10 @@ type SFTPBrowserTab struct { program *tea.Program + // Security + storagePassword string // master password for encrypted storage + passphraseCallback func() string // callback for SSH key passphrases + mu sync.Mutex closeOnce sync.Once } @@ -107,6 +111,14 @@ func (t *SFTPBrowserTab) SetProgram(p *tea.Program) { t.program = p } +func (t *SFTPBrowserTab) SetStoragePassword(password string) { + t.storagePassword = password +} + +func (t *SFTPBrowserTab) SetPassphraseCallback(cb func() string) { + t.passphraseCallback = cb +} + func (t *SFTPBrowserTab) Init() tea.Cmd { go t.connect() go t.refreshLocal() @@ -132,6 +144,11 @@ func (t *SFTPBrowserTab) connect() { timeout := 30 * time.Second cl := sshclient.NewClient(t.host, timeout) + // Wire up passphrase callback for encrypted keys + if t.passphraseCallback != nil { + cl.SetPassphraseCallback(t.passphraseCallback) + } + ctx := context.Background() if err := cl.Connect(ctx); err != nil { t.mu.Lock() diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 7f51dc5..99c9200 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -6,6 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" + "git.tukangketik.id/swanadiva/hostkeeper/pkg/knownhosts" "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" ) @@ -28,6 +29,10 @@ type Model struct { Quit bool dataDir string program *tea.Program + + // Security + storagePassword string + knownHosts *knownhosts.KnownHosts } // New creates a new TUI model @@ -53,6 +58,16 @@ func (m *Model) SetProgram(p *tea.Program) { m.program = p } +// SetStoragePassword sets the master password for encrypted storage +func (m *Model) SetStoragePassword(password string) { + m.storagePassword = password +} + +// SetKnownHosts sets the known_hosts manager for host key verification +func (m *Model) SetKnownHosts(kh *knownhosts.KnownHosts) { + m.knownHosts = kh +} + // Update handles messages and updates the model func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { @@ -91,6 +106,15 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.program != nil { tab.SetProgram(m.program) } + if m.storagePassword != "" { + tab.SetStoragePassword(m.storagePassword) + } + if m.knownHosts != nil { + tab.SetPassphraseCallback(func() string { + // TODO: prompt for passphrase in TUI + return "" + }) + } cmd := m.tabs.Add(tab) return m, cmd