Add SSHService, SFTPService, and CredentialService to the WraithApp struct. SSH service uses a no-op output handler (Wails event emission will be wired at runtime). CredentialService is created lazily after vault unlock. Both SSH and SFTP services are registered with Wails in main.go. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
196 lines
5.8 KiB
Go
196 lines
5.8 KiB
Go
package app
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/vstockwell/wraith/internal/connections"
|
|
"github.com/vstockwell/wraith/internal/credentials"
|
|
"github.com/vstockwell/wraith/internal/db"
|
|
"github.com/vstockwell/wraith/internal/plugin"
|
|
"github.com/vstockwell/wraith/internal/session"
|
|
"github.com/vstockwell/wraith/internal/settings"
|
|
"github.com/vstockwell/wraith/internal/sftp"
|
|
"github.com/vstockwell/wraith/internal/ssh"
|
|
"github.com/vstockwell/wraith/internal/theme"
|
|
"github.com/vstockwell/wraith/internal/vault"
|
|
)
|
|
|
|
// WraithApp is the main application struct that wires together all services
|
|
// and exposes vault management methods to the frontend via Wails bindings.
|
|
type WraithApp struct {
|
|
db *sql.DB
|
|
Vault *vault.VaultService
|
|
Settings *settings.SettingsService
|
|
Connections *connections.ConnectionService
|
|
Themes *theme.ThemeService
|
|
Sessions *session.Manager
|
|
Plugins *plugin.Registry
|
|
SSH *ssh.SSHService
|
|
SFTP *sftp.SFTPService
|
|
Credentials *credentials.CredentialService
|
|
unlocked bool
|
|
}
|
|
|
|
// New creates and initializes the WraithApp, opening the database, running
|
|
// migrations, creating all services, and seeding built-in themes.
|
|
func New() (*WraithApp, error) {
|
|
dataDir := dataDirectory()
|
|
dbPath := filepath.Join(dataDir, "wraith.db")
|
|
|
|
slog.Info("opening database", "path", dbPath)
|
|
database, err := db.Open(dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open database: %w", err)
|
|
}
|
|
|
|
if err := db.Migrate(database); err != nil {
|
|
return nil, fmt.Errorf("run migrations: %w", err)
|
|
}
|
|
|
|
settingsSvc := settings.NewSettingsService(database)
|
|
connSvc := connections.NewConnectionService(database)
|
|
themeSvc := theme.NewThemeService(database)
|
|
sessionMgr := session.NewManager()
|
|
pluginReg := plugin.NewRegistry()
|
|
|
|
// No-op output handler — Wails event emission will be wired at runtime.
|
|
sshSvc := ssh.NewSSHService(database, func(sessionID string, data []byte) {
|
|
// TODO: Emit Wails event "ssh:output" with sessionID + data
|
|
_ = sessionID
|
|
_ = data
|
|
})
|
|
sftpSvc := sftp.NewSFTPService()
|
|
|
|
// CredentialService requires the vault to be unlocked, so it starts nil.
|
|
// It is created lazily after the vault is unlocked via initCredentials().
|
|
|
|
// Seed built-in themes on every startup (INSERT OR IGNORE keeps it idempotent)
|
|
if err := themeSvc.SeedBuiltins(); err != nil {
|
|
slog.Warn("failed to seed themes", "error", err)
|
|
}
|
|
|
|
return &WraithApp{
|
|
db: database,
|
|
Settings: settingsSvc,
|
|
Connections: connSvc,
|
|
Themes: themeSvc,
|
|
Sessions: sessionMgr,
|
|
Plugins: pluginReg,
|
|
SSH: sshSvc,
|
|
SFTP: sftpSvc,
|
|
}, nil
|
|
}
|
|
|
|
// dataDirectory returns the path where Wraith stores its data.
|
|
// On Windows with APPDATA set, it uses %APPDATA%\Wraith.
|
|
// On macOS/Linux with XDG_DATA_HOME or HOME, it uses the appropriate path.
|
|
// Falls back to the current working directory for development.
|
|
func dataDirectory() string {
|
|
// Windows
|
|
if appData := os.Getenv("APPDATA"); appData != "" {
|
|
return filepath.Join(appData, "Wraith")
|
|
}
|
|
|
|
// macOS / Linux: use XDG_DATA_HOME or fallback to ~/.local/share
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
|
|
return filepath.Join(xdg, "wraith")
|
|
}
|
|
return filepath.Join(home, ".local", "share", "wraith")
|
|
}
|
|
|
|
// Dev fallback
|
|
return "."
|
|
}
|
|
|
|
// IsFirstRun checks whether the vault has been set up by looking for vault_salt in settings.
|
|
func (a *WraithApp) IsFirstRun() bool {
|
|
salt, _ := a.Settings.Get("vault_salt")
|
|
return salt == ""
|
|
}
|
|
|
|
// CreateVault sets up the vault with a master password. It generates a salt,
|
|
// derives an encryption key, and stores the salt and a check value in settings.
|
|
func (a *WraithApp) CreateVault(password string) error {
|
|
salt, err := vault.GenerateSalt()
|
|
if err != nil {
|
|
return fmt.Errorf("generate salt: %w", err)
|
|
}
|
|
|
|
key := vault.DeriveKey(password, salt)
|
|
a.Vault = vault.NewVaultService(key)
|
|
|
|
// Store salt as hex in settings
|
|
if err := a.Settings.Set("vault_salt", hex.EncodeToString(salt)); err != nil {
|
|
return fmt.Errorf("store salt: %w", err)
|
|
}
|
|
|
|
// Encrypt a known check value — used to verify the password on unlock
|
|
check, err := a.Vault.Encrypt("wraith-vault-check")
|
|
if err != nil {
|
|
return fmt.Errorf("encrypt check value: %w", err)
|
|
}
|
|
if err := a.Settings.Set("vault_check", check); err != nil {
|
|
return fmt.Errorf("store check value: %w", err)
|
|
}
|
|
|
|
a.unlocked = true
|
|
a.initCredentials()
|
|
slog.Info("vault created successfully")
|
|
return nil
|
|
}
|
|
|
|
// Unlock verifies the master password against the stored check value and
|
|
// initializes the vault service for decryption.
|
|
func (a *WraithApp) Unlock(password string) error {
|
|
saltHex, err := a.Settings.Get("vault_salt")
|
|
if err != nil || saltHex == "" {
|
|
return fmt.Errorf("vault not set up — call CreateVault first")
|
|
}
|
|
|
|
salt, err := hex.DecodeString(saltHex)
|
|
if err != nil {
|
|
return fmt.Errorf("decode salt: %w", err)
|
|
}
|
|
|
|
key := vault.DeriveKey(password, salt)
|
|
vs := vault.NewVaultService(key)
|
|
|
|
// Verify by decrypting the stored check value
|
|
checkEncrypted, err := a.Settings.Get("vault_check")
|
|
if err != nil || checkEncrypted == "" {
|
|
return fmt.Errorf("vault check value missing")
|
|
}
|
|
|
|
checkPlain, err := vs.Decrypt(checkEncrypted)
|
|
if err != nil {
|
|
return fmt.Errorf("incorrect master password")
|
|
}
|
|
if checkPlain != "wraith-vault-check" {
|
|
return fmt.Errorf("incorrect master password")
|
|
}
|
|
|
|
a.Vault = vs
|
|
a.unlocked = true
|
|
a.initCredentials()
|
|
slog.Info("vault unlocked successfully")
|
|
return nil
|
|
}
|
|
|
|
// IsUnlocked returns whether the vault is currently unlocked.
|
|
func (a *WraithApp) IsUnlocked() bool {
|
|
return a.unlocked
|
|
}
|
|
|
|
// initCredentials creates the CredentialService after the vault is unlocked.
|
|
func (a *WraithApp) initCredentials() {
|
|
if a.Vault != nil {
|
|
a.Credentials = credentials.NewCredentialService(a.db, a.Vault)
|
|
}
|
|
}
|