github.com/pluralsh/plural-cli@v0.9.5/cmd/plural/crypto.go (about)

     1  package plural
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/AlecAivazis/survey/v2"
    13  
    14  	"github.com/mitchellh/go-homedir"
    15  	"github.com/urfave/cli"
    16  
    17  	"github.com/pluralsh/plural-cli/pkg/api"
    18  	"github.com/pluralsh/plural-cli/pkg/crypto"
    19  	"github.com/pluralsh/plural-cli/pkg/scm"
    20  	"github.com/pluralsh/plural-cli/pkg/utils"
    21  	"github.com/pluralsh/plural-cli/pkg/utils/git"
    22  )
    23  
    24  var prefix = []byte("CHARTMART-ENCRYPTED")
    25  
    26  const (
    27  	GitAttributesFile = ".gitattributes"
    28  	GitIgnoreFile     = ".gitignore"
    29  )
    30  
    31  const Gitattributes = `/**/helm/**/values.yaml filter=plural-crypt diff=plural-crypt
    32  /**/helm/**/values.yaml* filter=plural-crypt diff=plural-crypt
    33  /**/helm/**/README.md* filter=plural-crypt diff=plural-crypt
    34  /**/helm/**/default-values.yaml* filter=plural-crypt diff=plural-crypt
    35  /**/terraform/**/main.tf filter=plural-crypt diff=plural-crypt
    36  /**/terraform/**/main.tf* filter=plural-crypt diff=plural-crypt
    37  /**/manifest.yaml filter=plural-crypt diff=plural-crypt
    38  /**/output.yaml filter=plural-crypt diff=plural-crypt
    39  /diffs/**/* filter=plural-crypt diff=plural-crypt
    40  context.yaml filter=plural-crypt diff=plural-crypt
    41  workspace.yaml filter=plural-crypt diff=plural-crypt
    42  context.yaml* filter=plural-crypt diff=plural-crypt
    43  workspace.yaml* filter=plural-crypt diff=plural-crypt
    44  helm-values/*.yaml filter=plural-crypt diff=plural-crypt
    45  .env filter=plural-crypt diff=plural-crypt
    46  .gitattributes !filter !diff
    47  `
    48  
    49  const Gitignore = `/**/.terraform
    50  /**/.terraform*
    51  /**/terraform.tfstate*
    52  /bin
    53  *~
    54  .idea
    55  *.swp
    56  *.swo
    57  .DS_STORE
    58  .vscode
    59  `
    60  
    61  // IMPORTANT
    62  // Repo cryptography relies on git smudge and clean filters, which pipe a file into stdin and respond with a new version
    63  // of the file from stdout. If we write anything besides the crypto text, it will no longer be decryptable naturally.
    64  func (p *Plural) cryptoCommands() []cli.Command {
    65  	return []cli.Command{
    66  		{
    67  			Name:   "encrypt",
    68  			Usage:  "encrypts stdin and writes to stdout",
    69  			Action: handleEncrypt,
    70  		},
    71  		{
    72  			Name:   "decrypt",
    73  			Usage:  "decrypts stdin and writes to stdout",
    74  			Action: handleDecrypt,
    75  		},
    76  		{
    77  			Name:   "init",
    78  			Usage:  "initializes git filters for you",
    79  			Action: cryptoInit,
    80  		},
    81  		{
    82  			Name:   "unlock",
    83  			Usage:  "auto-decrypts all affected files in the repo",
    84  			Action: handleUnlock,
    85  		},
    86  		{
    87  			Name:   "import",
    88  			Usage:  "imports an aes key for plural to use",
    89  			Action: importKey,
    90  		},
    91  		{
    92  			Name:   "recover",
    93  			Usage:  "recovers repo encryption keys from a working k8s cluster",
    94  			Action: initKubeconfig(p.handleRecover),
    95  		},
    96  		{
    97  			Name:   "random",
    98  			Usage:  "generates a random string",
    99  			Action: randString,
   100  			Flags: []cli.Flag{
   101  				cli.IntFlag{
   102  					Name:  "len",
   103  					Usage: "the length of the string to generate",
   104  					Value: 32,
   105  				},
   106  			},
   107  		},
   108  		{
   109  			Name:   "ssh-keygen",
   110  			Usage:  "generate an ed5519 keypair for use in git ssh",
   111  			Action: affirmed(handleKeygen, "This command will autogenerate an ed5519 keypair, without passphrase. Sound good?", "PLURAL_CRYPTO_SSH_KEYGEN"),
   112  		},
   113  		{
   114  			Name:   "export",
   115  			Usage:  "dumps the current aes key to stdout",
   116  			Action: exportKey,
   117  		},
   118  		{
   119  			Name:      "share",
   120  			Usage:     "allows a list of plural users to decrypt this repository",
   121  			ArgsUsage: "",
   122  			Flags: []cli.Flag{
   123  				cli.StringSliceFlag{
   124  					Name:     "email",
   125  					Usage:    "a email to share with (multiple allowed)",
   126  					Required: true,
   127  				},
   128  			},
   129  			Action: p.handleCryptoShare,
   130  		},
   131  		{
   132  			Name:  "setup-keys",
   133  			Usage: "creates an age keypair, and uploads the public key to plural for use in plural crypto share",
   134  			Flags: []cli.Flag{
   135  				cli.StringFlag{
   136  					Name:     "name",
   137  					Usage:    "a name for the key",
   138  					Required: true,
   139  				},
   140  			},
   141  			Action: p.handleSetupKeys,
   142  		},
   143  		{
   144  			Name:        "backups",
   145  			Usage:       "manages backups of your encryption keys",
   146  			Subcommands: p.backupCommands(),
   147  		},
   148  		{
   149  			Name:   "fingerprint",
   150  			Usage:  "generates a file with the key fingerprint",
   151  			Action: keyFingerprint,
   152  		},
   153  	}
   154  }
   155  
   156  func (p *Plural) backupCommands() []cli.Command {
   157  	return []cli.Command{
   158  		{
   159  			Name:   "list",
   160  			Usage:  "lists your current key backups",
   161  			Action: p.listBackups,
   162  		},
   163  		{
   164  			Name:   "create",
   165  			Usage:  "creates a backup for your current key",
   166  			Action: affirmed(p.createBackup, backupMsg, "PLURAL_BACKUPS_CREATE"),
   167  		},
   168  		{
   169  			Name:      "restore",
   170  			Usage:     "restores a key backup as your current encryption key",
   171  			ArgsUsage: "NAME",
   172  			Action:    requireArgs(p.restoreBackup, []string{"NAME"}),
   173  		},
   174  	}
   175  }
   176  
   177  func handleEncrypt(c *cli.Context) error {
   178  	data, err := io.ReadAll(os.Stdin)
   179  	if bytes.HasPrefix(data, prefix) {
   180  		_, err := os.Stdout.Write(data)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		return nil
   185  	}
   186  
   187  	if err != nil {
   188  		return err
   189  	}
   190  	cryptoProv, err := crypto.Build()
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	result, err := crypto.Encrypt(cryptoProv, data)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	_, err = os.Stdout.Write(prefix)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	_, err = os.Stdout.Write(result)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	return nil
   208  }
   209  
   210  func handleDecrypt(c *cli.Context) error {
   211  	var file io.Reader
   212  	if c.Args().Present() {
   213  		p, _ := filepath.Abs(c.Args().First())
   214  		f, err := os.Open(p)
   215  		defer func(f *os.File) {
   216  			_ = f.Close()
   217  		}(f)
   218  		if err != nil {
   219  			return err
   220  		}
   221  		file = f
   222  	} else {
   223  		file = os.Stdin
   224  	}
   225  
   226  	data, err := io.ReadAll(file)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	if !bytes.HasPrefix(data, prefix) {
   231  		_, err := os.Stdout.Write(data)
   232  		if err != nil {
   233  			return err
   234  		}
   235  		return nil
   236  	}
   237  
   238  	prov, err := crypto.Build()
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	result, err := crypto.Decrypt(prov, data[len(prefix):])
   244  	if err != nil {
   245  		return err
   246  	}
   247  
   248  	_, err = os.Stdout.Write(result)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	return nil
   253  }
   254  
   255  // CheckGitCrypt method checks if the .gitattributes and .gitignore files exist and have desired content.
   256  // Some old repos can have fewer files to encrypt and must be updated.
   257  func CheckGitCrypt(c *cli.Context) error {
   258  	if !utils.Exists(GitAttributesFile) || !utils.Exists(GitIgnoreFile) {
   259  		return cryptoInit(c)
   260  	}
   261  	toCompare := map[string]string{GitAttributesFile: Gitattributes, GitIgnoreFile: Gitignore}
   262  
   263  	for file, content := range toCompare {
   264  		equal, err := utils.CompareFileContent(file, content)
   265  		if err != nil {
   266  			return err
   267  		}
   268  		if !equal {
   269  			return cryptoInit(c)
   270  		}
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  func cryptoInit(c *cli.Context) error {
   277  	encryptConfig := [][]string{
   278  		{"filter.plural-crypt.smudge", "plural crypto decrypt"},
   279  		{"filter.plural-crypt.clean", "plural crypto encrypt"},
   280  		{"filter.plural-crypt.required", "true"},
   281  		{"diff.plural-crypt.textconv", "plural crypto decrypt"},
   282  	}
   283  
   284  	utils.Highlight("Creating git encryption filters\n")
   285  	for _, conf := range encryptConfig {
   286  		if err := gitConfig(conf[0], conf[1]); err != nil {
   287  			return err
   288  		}
   289  	}
   290  
   291  	if err := utils.WriteFile(GitAttributesFile, []byte(Gitattributes)); err != nil {
   292  		return err
   293  	}
   294  
   295  	if err := utils.WriteFile(GitIgnoreFile, []byte(Gitignore)); err != nil {
   296  		return err
   297  	}
   298  
   299  	_, err := crypto.Build()
   300  	return err
   301  }
   302  
   303  func (p *Plural) handleCryptoShare(c *cli.Context) error {
   304  	p.InitPluralClient()
   305  	emails := c.StringSlice("email")
   306  	if err := crypto.SetupAge(p.Client, emails); err != nil {
   307  		return err
   308  	}
   309  
   310  	prov, err := crypto.BuildAgeProvider()
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	return crypto.Flush(prov)
   316  }
   317  
   318  func (p *Plural) handleSetupKeys(c *cli.Context) error {
   319  	p.InitPluralClient()
   320  	name := c.String("name")
   321  	if err := crypto.SetupIdentity(p.Client, name); err != nil {
   322  		return err
   323  	}
   324  
   325  	utils.Success("Public key uploaded successfully\n")
   326  	return nil
   327  }
   328  
   329  func handleUnlock(c *cli.Context) error {
   330  	_, err := crypto.Build()
   331  	if err != nil {
   332  		return err
   333  	}
   334  
   335  	repoRoot, err := git.Root()
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	// fixes Invalid cross-device link when using os.Rename
   341  	gitIndexDir, err := filepath.Abs(filepath.Join(repoRoot, ".git"))
   342  	if err != nil {
   343  		return err
   344  	}
   345  	gitIndex := filepath.Join(gitIndexDir, "index")
   346  	dump, err := os.CreateTemp(gitIndexDir, "index.bak")
   347  	if err != nil {
   348  		return err
   349  	}
   350  	if err := os.Rename(gitIndex, dump.Name()); err != nil {
   351  		return err
   352  	}
   353  
   354  	if err := gitCommand("checkout", "HEAD", "--", repoRoot).Run(); err != nil {
   355  		_ = os.Rename(dump.Name(), gitIndex)
   356  		return errUnlock
   357  	}
   358  
   359  	os.Remove(dump.Name())
   360  	return nil
   361  }
   362  
   363  func exportKey(c *cli.Context) error {
   364  	key, err := crypto.Materialize()
   365  	if err != nil {
   366  		return err
   367  	}
   368  	marshal, err := key.Marshal()
   369  	if err != nil {
   370  		return err
   371  	}
   372  	_, err = os.Stdout.Write(marshal)
   373  	if err != nil {
   374  		return err
   375  	}
   376  	return nil
   377  }
   378  
   379  func importKey(c *cli.Context) error {
   380  	data, err := io.ReadAll(os.Stdin)
   381  	if err != nil {
   382  		return err
   383  	}
   384  	key, err := crypto.Import(data)
   385  	if err != nil {
   386  		return err
   387  	}
   388  	return key.Flush()
   389  }
   390  
   391  func randString(c *cli.Context) error {
   392  	var err error
   393  	intVar := c.Int("len")
   394  	len := c.Args().Get(0)
   395  	if len != "" {
   396  		intVar, err = strconv.Atoi(len)
   397  		if err != nil {
   398  			return err
   399  		}
   400  	}
   401  	str, err := crypto.RandStr(intVar)
   402  	if err != nil {
   403  		return err
   404  	}
   405  
   406  	fmt.Println(str)
   407  	return nil
   408  }
   409  
   410  func handleKeygen(c *cli.Context) error {
   411  	path, err := homedir.Expand("~/.ssh")
   412  	if err != nil {
   413  		return err
   414  	}
   415  
   416  	pub, priv, err := scm.GenerateKeys(false)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	filename, ok := utils.GetEnvStringValue("PLURAL_CRYPTO_KEYPAIR_NAME")
   422  	if !ok {
   423  		input := &survey.Input{Message: "What do you want to name your keypair?", Default: "id_plrl"}
   424  		err = survey.AskOne(input, &filename, survey.WithValidator(func(val interface{}) error {
   425  			name, _ := val.(string)
   426  			if utils.Exists(filepath.Join(path, name)) {
   427  				return fmt.Errorf("File ~/.ssh/%s already exists", name)
   428  			}
   429  
   430  			return nil
   431  		}))
   432  		if err != nil {
   433  			return err
   434  		}
   435  	}
   436  
   437  	if err := os.WriteFile(filepath.Join(path, filename), []byte(priv), 0600); err != nil {
   438  		return err
   439  	}
   440  
   441  	if err := os.WriteFile(filepath.Join(path, filename+".pub"), []byte(pub), 0644); err != nil {
   442  		return err
   443  	}
   444  
   445  	return nil
   446  }
   447  
   448  func (p *Plural) handleRecover(c *cli.Context) error {
   449  	if err := p.InitKube(); err != nil {
   450  		return err
   451  	}
   452  
   453  	secret, err := p.Secret("console", "console-conf")
   454  	if err != nil {
   455  		return err
   456  	}
   457  
   458  	key, ok := secret.Data["key"]
   459  	if !ok {
   460  		return fmt.Errorf("could not find `key` in console-conf secret")
   461  	}
   462  
   463  	aesKey, err := crypto.Import(key)
   464  	if err != nil {
   465  		return err
   466  	}
   467  
   468  	if err := crypto.Setup(aesKey.Key); err != nil {
   469  		return err
   470  	}
   471  
   472  	utils.Success("Key successfully synced locally!\n")
   473  	fmt.Println("you might need to run `plural crypto init` and `plural crypto setup-keys` to decrypt any repos with your new key")
   474  	return nil
   475  }
   476  
   477  func (p *Plural) listBackups(c *cli.Context) error {
   478  	p.InitPluralClient()
   479  
   480  	backups, err := p.Client.ListKeyBackups()
   481  	if err != nil {
   482  		return api.GetErrorResponse(err, "ListKeyBackups")
   483  	}
   484  
   485  	headers := []string{"Name", "Repositories", "Digest", "Created On"}
   486  	return utils.PrintTable(backups, headers, func(back *api.KeyBackup) ([]string, error) {
   487  		return []string{back.Name, strings.Join(back.Repositories, ", "), back.Digest, back.InsertedAt}, nil
   488  	})
   489  }
   490  
   491  func (p *Plural) createBackup(c *cli.Context) error {
   492  	p.InitPluralClient()
   493  	return crypto.BackupKey(p.Client)
   494  }
   495  
   496  func (p *Plural) restoreBackup(c *cli.Context) error {
   497  	p.InitPluralClient()
   498  	name := c.Args().First()
   499  	return crypto.DownloadBackup(p.Client, name)
   500  }
   501  
   502  func keyFingerprint(_ *cli.Context) error {
   503  	return crypto.CreateKeyFingerprintFile()
   504  }