feat: integrate security features into TUI

- SFTPBrowserTab: added storagePassword, passphraseCallback fields
- Model: added storagePassword, knownHosts fields
- cmd/hostkeeper/tui.go: initialize known_hosts on startup
- SFTP connect: wire passphrase callback to SSH client
- SetStoragePassword/SetKnownHosts/SetPassphraseCallback methods
This commit is contained in:
swanadiva
2026-06-25 13:32:59 +07:00
parent 611b794fc7
commit 1d2e29d20a
3 changed files with 51 additions and 0 deletions
+10
View File
@@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config" "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/storage"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/tui" "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) 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() ctx := context.Background()
hosts, err := store.ListHosts(ctx) hosts, err := store.ListHosts(ctx)
if err != nil { if err != nil {
@@ -48,6 +55,9 @@ func runTUI(cmd *cobra.Command, args []string) error {
model := tui.New() model := tui.New()
model.SetDataDir(cfg.GetDataDir()) model.SetDataDir(cfg.GetDataDir())
model.LoadHosts(hosts) model.LoadHosts(hosts)
if kh != nil {
model.SetKnownHosts(kh)
}
p := tea.NewProgram(model) p := tea.NewProgram(model)
model.SetProgram(p) model.SetProgram(p)
+17
View File
@@ -74,6 +74,10 @@ type SFTPBrowserTab struct {
program *tea.Program program *tea.Program
// Security
storagePassword string // master password for encrypted storage
passphraseCallback func() string // callback for SSH key passphrases
mu sync.Mutex mu sync.Mutex
closeOnce sync.Once closeOnce sync.Once
} }
@@ -107,6 +111,14 @@ func (t *SFTPBrowserTab) SetProgram(p *tea.Program) {
t.program = p 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 { func (t *SFTPBrowserTab) Init() tea.Cmd {
go t.connect() go t.connect()
go t.refreshLocal() go t.refreshLocal()
@@ -132,6 +144,11 @@ func (t *SFTPBrowserTab) connect() {
timeout := 30 * time.Second timeout := 30 * time.Second
cl := sshclient.NewClient(t.host, timeout) cl := sshclient.NewClient(t.host, timeout)
// Wire up passphrase callback for encrypted keys
if t.passphraseCallback != nil {
cl.SetPassphraseCallback(t.passphraseCallback)
}
ctx := context.Background() ctx := context.Background()
if err := cl.Connect(ctx); err != nil { if err := cl.Connect(ctx); err != nil {
t.mu.Lock() t.mu.Lock()
+24
View File
@@ -6,6 +6,7 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"git.tukangketik.id/swanadiva/hostkeeper/internal/models" "git.tukangketik.id/swanadiva/hostkeeper/internal/models"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/knownhosts"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
) )
@@ -28,6 +29,10 @@ type Model struct {
Quit bool Quit bool
dataDir string dataDir string
program *tea.Program program *tea.Program
// Security
storagePassword string
knownHosts *knownhosts.KnownHosts
} }
// New creates a new TUI model // New creates a new TUI model
@@ -53,6 +58,16 @@ func (m *Model) SetProgram(p *tea.Program) {
m.program = p 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 // Update handles messages and updates the model
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -91,6 +106,15 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.program != nil { if m.program != nil {
tab.SetProgram(m.program) 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) cmd := m.tabs.Add(tab)
return m, cmd return m, cmd