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:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user