cli/pkg/ssh/ssh_keys.go
Kynan Ware b8ee5c5eba Add godoc comments to exported symbols in remaining packages
Add documentation comments to exported symbols across all remaining
smaller packages including cmd/gen-docs, context, internal/browser,
internal/gh, internal/ghcmd, internal/ghinstance, internal/ghrepo,
internal/keyring, internal/run, internal/safepaths, internal/tableprinter,
internal/text, internal/update, pkg/cmd/accessibility, pkg/cmd/actions,
pkg/cmd/alias, pkg/cmd/api, pkg/cmd/browse, pkg/cmd/cache,
pkg/cmd/completion, pkg/cmd/copilot, pkg/cmd/factory, pkg/cmd/gpg-key,
pkg/cmd/label, pkg/cmd/licenses, pkg/cmd/org, pkg/cmd/preview,
pkg/cmd/ssh-key, pkg/cmd/version, pkg/extensions, pkg/jsoncolor,
pkg/markdown, pkg/option, pkg/set, pkg/ssh, pkg/surveyext, test,
internal/authflow, and utils.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 16:14:20 -07:00

129 lines
3 KiB
Go

package ssh
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/safeexec"
)
// Context holds configuration for SSH key operations.
type Context struct {
configDir string
keygenExe string
}
// NewContextForTests creates a new `ssh.Context` with internal properties set to the
// specified values. It should only be used to inject test-specific setup.
func NewContextForTests(configDir, keygenExe string) Context {
return Context{
configDir,
keygenExe,
}
}
// KeyPair holds a public and private SSH key pair.
type KeyPair struct {
PublicKeyPath string
PrivateKeyPath string
}
// ErrKeyAlreadyExists is returned when an SSH key already exists.
var ErrKeyAlreadyExists = errors.New("SSH key already exists")
// LocalPublicKeys returns the list of public SSH keys found locally.
func (c *Context) LocalPublicKeys() ([]string, error) {
sshDir, err := c.SshDir()
if err != nil {
return nil, err
}
return filepath.Glob(filepath.Join(sshDir, "*.pub"))
}
// HasKeygen reports whether ssh-keygen is available.
func (c *Context) HasKeygen() bool {
_, err := c.findKeygen()
return err == nil
}
// GenerateSSHKey generates a new SSH key pair.
func (c *Context) GenerateSSHKey(keyName string, passphrase string) (*KeyPair, error) {
keygenExe, err := c.findKeygen()
if err != nil {
return nil, err
}
sshDir, err := c.SshDir()
if err != nil {
return nil, err
}
err = os.MkdirAll(sshDir, 0700)
if err != nil {
return nil, fmt.Errorf("could not create .ssh directory: %w", err)
}
keyFile := filepath.Join(sshDir, keyName)
keyPair := KeyPair{
PublicKeyPath: keyFile + ".pub",
PrivateKeyPath: keyFile,
}
if _, err := os.Stat(keyFile); err == nil {
// Still return keyPair because the caller might be OK with this - they can check the error with errors.Is(err, ErrKeyAlreadyExists)
return &keyPair, ErrKeyAlreadyExists
}
if err := os.MkdirAll(filepath.Dir(keyFile), 0711); err != nil {
return nil, err
}
keygenCmd := exec.Command(keygenExe, "-t", "ed25519", "-C", "", "-N", passphrase, "-f", keyFile)
err = run.PrepareCmd(keygenCmd).Run()
if err != nil {
return nil, err
}
return &keyPair, nil
}
// SshDir returns the path to the SSH directory.
func (c *Context) SshDir() (string, error) {
if c.configDir != "" {
return c.configDir, nil
}
dir, err := config.HomeDirPath(".ssh")
if err == nil {
c.configDir = dir
}
return dir, err
}
func (c *Context) findKeygen() (string, error) {
if c.keygenExe != "" {
return c.keygenExe, nil
}
keygenExe, err := safeexec.LookPath("ssh-keygen")
if err != nil && runtime.GOOS == "windows" {
// We can try and find ssh-keygen in a Git for Windows install
if gitPath, err := safeexec.LookPath("git"); err == nil {
gitKeygen := filepath.Join(filepath.Dir(gitPath), "..", "usr", "bin", "ssh-keygen.exe")
if _, err = os.Stat(gitKeygen); err == nil {
return gitKeygen, nil
}
}
}
if err == nil {
c.keygenExe = keygenExe
}
return keygenExe, err
}