847989df75
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/ - Create v1/README.md with V1 documentation - Update root README for V1 + V2 structure - V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass - Root is now clean for V2 development
139 lines
3.6 KiB
Go
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
|
|
} |