Files
swanadiva 611b794fc7 feat: Phase 2 Security Enhancement
- pkg/crypto: AES-256-GCM encryption with PBKDF2 key derivation
  - 100k iterations, 16-byte salt, SHA-256
  - Encrypt/Decrypt/IsEncrypted/HashPassword
- Storage layer encryption:
  - JSONStorage.SetPassword() enables transparent encrypt/decrypt
  - readJSON auto-decrypts, replace* auto-encrypts
- pkg/knownhosts: TOFU host key verification
  - Verify/Add/Remove host keys
  - HostKeyCallback for SSH config
- SSH client security:
  - SetHostKeyCallback() replaces InsecureIgnoreHostKey()
  - SetPassphraseCallback() for encrypted private keys
  - getKeySigner() tries passphrase on encrypted keys
- Models: AppConfig gains EncryptionEnabled, PasswordHash, KnownHostsFile
2026-06-25 13:28:46 +07:00

139 lines
3.6 KiB
Go

package ssh
import (
"fmt"
"os"
"path/filepath"
"strings"
cryptossh "golang.org/x/crypto/ssh"
)
// getAuthMethods returns SSH authentication methods based on host config
func (c *Client) getAuthMethods() ([]cryptossh.AuthMethod, error) {
var authMethods []cryptossh.AuthMethod
switch c.host.Auth.Type {
case "password":
if c.host.Auth.Password == "" {
return nil, fmt.Errorf("password auth requires password")
}
authMethods = append(authMethods, cryptossh.Password(c.host.Auth.Password))
case "key":
signer, err := c.getKeySigner()
if err != nil {
return nil, fmt.Errorf("failed to setup key authentication: %w", err)
}
authMethods = append(authMethods, cryptossh.PublicKeys(signer))
case "both":
// Try password first
if c.host.Auth.Password != "" {
authMethods = append(authMethods, cryptossh.Password(c.host.Auth.Password))
}
// Then try key
signer, err := c.getKeySigner()
if err == nil {
authMethods = append(authMethods, cryptossh.PublicKeys(signer))
}
default:
return nil, fmt.Errorf("unsupported authentication type: %s", c.host.Auth.Type)
}
if len(authMethods) == 0 {
return nil, fmt.Errorf("no authentication methods configured")
}
return authMethods, nil
}
// getKeySigner returns an SSH signer for key-based authentication
//
// Phase 1: Loads key from KeyID as a file path, or from common SSH key locations
// Phase 2: Will integrate with the storage layer for encrypted key storage
func (c *Client) getKeySigner() (cryptossh.Signer, error) {
var keyPath string
if c.host.Auth.KeyID != "" {
// If KeyID looks like a path, use it directly
if strings.HasPrefix(c.host.Auth.KeyID, "/") || strings.HasPrefix(c.host.Auth.KeyID, "~") {
keyPath = expandPath(c.host.Auth.KeyID)
} else {
// Try ~/.ssh/<keyID>
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
keyPath = filepath.Join(home, ".ssh", c.host.Auth.KeyID)
}
} else {
// Fall back to default key locations
keyPath = getDefaultKeyPath()
}
// Read the key file
keyData, err := os.ReadFile(keyPath) //nolint:gosec
if err != nil {
return nil, fmt.Errorf("failed to read key file %s: %w", keyPath, err)
}
// Parse the key (support passphrase-protected keys)
var signer cryptossh.Signer
if c.host.Auth.Password != "" {
signer, err = cryptossh.ParsePrivateKeyWithPassphrase(keyData, []byte(c.host.Auth.Password))
} else if c.passphraseCallback != nil {
// Try without passphrase first
signer, err = cryptossh.ParsePrivateKey(keyData)
if err != nil && strings.Contains(err.Error(), "encrypted") {
// Key is encrypted, prompt for passphrase
passphrase := c.passphraseCallback()
if passphrase != "" {
signer, err = cryptossh.ParsePrivateKeyWithPassphrase(keyData, []byte(passphrase))
}
}
} else {
signer, err = cryptossh.ParsePrivateKey(keyData)
}
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
return signer, nil
}
// getDefaultKeyPath returns the first existing default SSH key path
func getDefaultKeyPath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
candidates := []string{
"id_ed25519",
"id_rsa",
"id_ecdsa",
"id_dsa",
}
for _, name := range candidates {
path := filepath.Join(home, ".ssh", name)
if _, err := os.Stat(path); err == nil {
return path
}
}
return ""
}
// expandPath expands ~ to the home directory
func expandPath(path string) string {
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err == nil {
return filepath.Join(home, path[2:])
}
}
return path
}