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 }