pax_global_header00006660000000000000000000000064141176757630014533gustar00rootroot0000000000000052 comment=caa79f49edbad448d52688c1c043fb647a4c619c keygen-0.1.2/000077500000000000000000000000001411767576300130155ustar00rootroot00000000000000keygen-0.1.2/.github/000077500000000000000000000000001411767576300143555ustar00rootroot00000000000000keygen-0.1.2/.github/workflows/000077500000000000000000000000001411767576300164125ustar00rootroot00000000000000keygen-0.1.2/.github/workflows/build.yml000066400000000000000000000011111411767576300202260ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: test: strategy: matrix: go-version: [~1.17] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Test run: go test ./... keygen-0.1.2/.github/workflows/lint.yml000066400000000000000000000010561411767576300201050ustar00rootroot00000000000000name: lint on: [push, pull_request] jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Optional: golangci-lint command line arguments. args: --issues-exit-code=0 # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true keygen-0.1.2/LICENSE000066400000000000000000000020631411767576300140230ustar00rootroot00000000000000MIT License Copyright (c) 2021 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. keygen-0.1.2/README.md000066400000000000000000000022631411767576300142770ustar00rootroot00000000000000# Keygen [![Latest Release](https://img.shields.io/github/release/charmbracelet/keygen.svg)](https://github.com/charmbracelet/keygen/releases) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/keygen?tab=doc) [![Build Status](https://github.com/charmbracelet/keygen/workflows/build/badge.svg)](https://github.com/charmbracelet/keygen/actions) [![Go ReportCard](https://goreportcard.com/badge/charmbracelet/keygen)](https://goreportcard.com/report/charmbracelet/keygen) An SSH key pair generator. Supports generating RSA and Ed25519 keys. ## Example ```go k, err := NewSSHKeyPair(".ssh", "my_awesome_key", []byte(""), key.Ed25519) if err != nil { fmt.Printf("error creating SSH key pair: %v", err) os.Exit(1) } if !k.KeyPairExist() { err = k.WriteKeys() if err != nil { fmt.Printf("error writing SSH key pair: %v", err) os.Exit(1) } } ``` ## License [MIT](https://github.com/charmbracelet/keygen/raw/master/LICENSE) *** Part of [Charm](https://charm.sh). the Charm logo Charm热爱开源 • Charm loves open source keygen-0.1.2/go.mod000066400000000000000000000004461411767576300141270ustar00rootroot00000000000000module github.com/charmbracelet/keygen go 1.17 require ( github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mitchellh/go-homedir v1.1.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 ) require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect keygen-0.1.2/go.sum000066400000000000000000000026421411767576300141540ustar00rootroot00000000000000github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= keygen-0.1.2/keygen.go000066400000000000000000000171041411767576300146310ustar00rootroot00000000000000// Package keygen handles the creation of new SSH key pairs. package keygen import ( "bytes" "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "fmt" "io/ioutil" "os" "os/user" "path/filepath" "github.com/mikesmitty/edkey" "github.com/mitchellh/go-homedir" "golang.org/x/crypto/ssh" ) // KeyType represents a type of SSH key. type KeyType string // Supported key types. const ( RSA KeyType = "rsa" Ed25519 KeyType = "ed25519" ) const rsaDefaultBits = 4096 // ErrMissingSSHKeys indicates we're missing some keys that we expected to // have after generating. This should be an extreme edge case. var ErrMissingSSHKeys = errors.New("missing one or more keys; did something happen to them after they were generated?") // FilesystemErr is used to signal there was a problem creating keys at the // filesystem-level. For example, when we're unable to create a directory to // store new SSH keys in. type FilesystemErr struct { Err error } // Error returns a human-readable string for the erorr. It implements the error // interface. func (e FilesystemErr) Error() string { return e.Err.Error() } // Unwrap returne the underlying error. func (e FilesystemErr) Unwrap() error { return e.Err } // SSHKeysAlreadyExistErr indicates that files already exist at the location at // which we're attempting to create SSH keys. type SSHKeysAlreadyExistErr struct { Path string } // SSHKeyPair holds a pair of SSH keys and associated methods. type SSHKeyPair struct { PrivateKeyPEM []byte PublicKey []byte KeyDir string Filename string // private key filename; public key will have .pub appended } func (s SSHKeyPair) privateKeyPath() string { return filepath.Join(s.KeyDir, s.Filename) } func (s SSHKeyPair) publicKeyPath() string { return filepath.Join(s.KeyDir, s.Filename+".pub") } // New generates an SSHKeyPair, which contains a pair of SSH keys. func New(path, name string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) { var err error s := &SSHKeyPair{ KeyDir: path, Filename: fmt.Sprintf("%s_%s", name, keyType), } if s.IsKeyPairExists() { pubData, err := ioutil.ReadFile(s.publicKeyPath()) if err != nil { return nil, err } s.PublicKey = pubData privData, err := ioutil.ReadFile(s.privateKeyPath()) if err != nil { return nil, err } s.PrivateKeyPEM = privData return s, nil } switch keyType { case Ed25519: err = s.generateEd25519Keys() case RSA: err = s.generateRSAKeys(rsaDefaultBits, passphrase) default: return nil, fmt.Errorf("unsupported key type %s", keyType) } if err != nil { return nil, err } return s, nil } // New generates an SSHKeyPair and writes it to disk if not exist. func NewWithWrite(path, name string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) { s, err := New(path, name, passphrase, keyType) if err != nil { return nil, err } if !s.IsKeyPairExists() { if err = s.WriteKeys(); err != nil { return nil, err } } return s, nil } // generateEd25519Keys creates a pair of EdD25519 keys for SSH auth. func (s *SSHKeyPair) generateEd25519Keys() error { // Generate keys pubKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return err } // Encode PEM pemBlock := pem.EncodeToMemory(&pem.Block{ Type: "OPENSSH PRIVATE KEY", Bytes: edkey.MarshalED25519PrivateKey(privateKey), }) // Prepare public key publicKey, err := ssh.NewPublicKey(pubKey) if err != nil { return err } // serialize for public key file on disk serializedPublicKey := ssh.MarshalAuthorizedKey(publicKey) s.PrivateKeyPEM = pemBlock s.PublicKey = pubKeyWithMemo(serializedPublicKey) return nil } // generateRSAKeys creates a pair for RSA keys for SSH auth. func (s *SSHKeyPair) generateRSAKeys(bitSize int, passphrase []byte) error { // Generate private key privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) if err != nil { return err } // Validate private key err = privateKey.Validate() if err != nil { return err } // Get ASN.1 DER format x509Encoded := x509.MarshalPKCS1PrivateKey(privateKey) block := &pem.Block{ Type: "RSA PRIVATE KEY", Headers: nil, Bytes: x509Encoded, } // encrypt private key with passphrase if len(passphrase) > 0 { block, err = x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, passphrase, x509.PEMCipherAES256) if err != nil { return err } } // Private key in PEM format pemBlock := pem.EncodeToMemory(block) // Generate public key publicRSAKey, err := ssh.NewPublicKey(privateKey.Public()) if err != nil { return err } // serialize for public key file on disk serializedPubKey := ssh.MarshalAuthorizedKey(publicRSAKey) s.PrivateKeyPEM = pemBlock s.PublicKey = pubKeyWithMemo(serializedPubKey) return nil } // prepFilesystem makes sure the state of the filesystem is as it needs to be // in order to write our keys to disk. It will create and/or set permissions on // the SSH directory we're going to write our keys to (for example, ~/.ssh) as // well as make sure that no files exist at the location in which we're going // to write out keys. func (s *SSHKeyPair) prepFilesystem() error { var err error s.KeyDir, err = homedir.Expand(s.KeyDir) if err != nil { return err } info, err := os.Stat(s.KeyDir) if os.IsNotExist(err) { // Directory doesn't exist: create it return os.MkdirAll(s.KeyDir, 0700) } if err != nil { // There was another error statting the directory; something is awry return FilesystemErr{Err: err} } if !info.IsDir() { // It exists but it's not a directory return FilesystemErr{Err: fmt.Errorf("%s is not a directory", s.KeyDir)} } if info.Mode().Perm() != 0700 { // Permissions are wrong: fix 'em if err := os.Chmod(s.KeyDir, 0700); err != nil { return FilesystemErr{Err: err} } } // Make sure the files we're going to write to don't already exist if fileExists(s.privateKeyPath()) { return SSHKeysAlreadyExistErr{Path: s.privateKeyPath()} } if fileExists(s.publicKeyPath()) { return SSHKeysAlreadyExistErr{Path: s.publicKeyPath()} } // The directory looks good as-is return nil } // WriteKeys writes the SSH key pair to disk. func (s *SSHKeyPair) WriteKeys() error { if len(s.PrivateKeyPEM) == 0 || len(s.PublicKey) == 0 { return ErrMissingSSHKeys } if err := s.prepFilesystem(); err != nil { return err } if err := writeKeyToFile(s.PrivateKeyPEM, s.privateKeyPath()); err != nil { return err } if err := writeKeyToFile(s.PublicKey, s.publicKeyPath()); err != nil { return err } return nil } func (s *SSHKeyPair) IsKeyPairExists() bool { return fileExists(s.privateKeyPath()) && fileExists(s.publicKeyPath()) } func writeKeyToFile(keyBytes []byte, path string) error { _, err := os.Stat(path) if os.IsNotExist(err) { return ioutil.WriteFile(path, keyBytes, 0600) } return FilesystemErr{Err: fmt.Errorf("file %s already exists", path)} } func fileExists(path string) bool { _, err := os.Stat(path) if os.IsNotExist(err) { return false } if err != nil { return false } return true } // attaches a user@host suffix to a serialized public key. returns the original // pubkey if we can't get the username or host. func pubKeyWithMemo(pubKey []byte) []byte { u, err := user.Current() if err != nil { return pubKey } hostname, err := os.Hostname() if err != nil { return pubKey } return append(bytes.TrimRight(pubKey, "\n"), []byte(fmt.Sprintf(" %s@%s\n", u.Username, hostname))...) } // Error returns the a human-readable error message for SSHKeysAlreadyExistErr. // It satisfies the error interface. func (e SSHKeysAlreadyExistErr) Error() string { return fmt.Sprintf("ssh key %s already exists", e.Path) } keygen-0.1.2/keygen_test.go000066400000000000000000000050641411767576300156720ustar00rootroot00000000000000package keygen import ( "os" "path/filepath" "testing" ) func TestNewSSHKeyPair(t *testing.T) { dir := t.TempDir() _, err := NewWithWrite(dir, "test", []byte(""), "rsa") if err != nil { t.Errorf("error creating SSH key pair: %v", err) } } func TestGenerateEd25519Keys(t *testing.T) { // Create temp directory for keys dir := t.TempDir() k := &SSHKeyPair{ KeyDir: dir, Filename: "test", } t.Run("test generate SSH keys", func(t *testing.T) { err := k.generateEd25519Keys() if err != nil { t.Errorf("error creating SSH key pair: %v", err) } // TODO: is there a good way to validate these? Lengths seem to vary a bit, // so far now we're just asserting that the keys indeed exist. if len(k.PrivateKeyPEM) == 0 { t.Error("error creating SSH private key PEM; key is 0 bytes") } if len(k.PublicKey) == 0 { t.Error("error creating SSH public key; key is 0 bytes") } }) t.Run("test write SSH keys", func(t *testing.T) { k.KeyDir = filepath.Join(dir, "ssh1") if err := k.prepFilesystem(); err != nil { t.Errorf("filesystem error: %v\n", err) } if err := k.WriteKeys(); err != nil { t.Errorf("error writing SSH keys to %s: %v", k.KeyDir, err) } if testing.Verbose() { t.Logf("Wrote keys to %s", k.KeyDir) } }) t.Run("test not overwriting existing keys", func(t *testing.T) { k.KeyDir = filepath.Join(dir, "ssh2") if err := k.prepFilesystem(); err != nil { t.Errorf("filesystem error: %v\n", err) } // Private key filePath := filepath.Join(k.KeyDir, k.Filename) if !createEmptyFile(t, filePath) { return } if err := k.WriteKeys(); err == nil { t.Errorf("we wrote the private key over an existing file, but we were not supposed to") } if err := os.Remove(filePath); err != nil { t.Errorf("could not remove file %s", filePath) } // Public key if !createEmptyFile(t, filePath+".pub") { return } if err := k.WriteKeys(); err == nil { t.Errorf("we wrote the public key over an existing file, but we were not supposed to") } }) } // touchTestFile is a utility function we're using in testing. func createEmptyFile(t *testing.T, path string) (ok bool) { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0700); err != nil { t.Errorf("could not create directory %s: %v", dir, err) return false } f, err := os.Create(path) if err != nil { t.Errorf("could not create file %s", path) return false } if err := f.Close(); err != nil { t.Errorf("could not close file: %v", err) return false } if testing.Verbose() { t.Logf("created dummy file at %s", path) } return true }