diff --git a/.gitignore b/.gitignore index f1c181e..c588405 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out + +build +testdata +vault-key +.idea \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0bee326 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the maintainers of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before submitting a Pull Request. +2. Update the [README.md](README.md) with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0f60213 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +NAME=gwvault + +VERSION=$$(git describe --tags --always) + +LDFLAGS=-ldflags "-X main.version=${VERSION}" + +all: tools build + +tools: + go get -u -v "github.com/mitchellh/gox" + +build: + @mkdir -p bin/ + go get -t ./... + go test -v ./... + go build ${LDFLAGS} -o bin/${NAME} cmd/gwvault/main.go + +xbuild: clean + @mkdir -p build + gox \ + -os="linux" \ + -os="windows" \ + -os="darwin" \ + -arch="amd64" \ + ${LDFLAGS} \ + -output="build/{{.Dir}}_$(VERSION)_{{.OS}}_{{.Arch}}/$(NAME)" \ + ./... + +package: xbuild + $(eval FILES := $(shell ls build)) + @mkdir -p build/tgz + for f in $(FILES); do \ + (cd $(shell pwd)/build && tar -zcvf tgz/$$f.tar.gz $$f); \ + echo $$f; \ + done + +clean: + @rm -rf bin/ && rm -rf build/ + +ci: tools package + +.PHONY: all tools build xbuild package clean ci diff --git a/README.md b/README.md index d952418..d1cb190 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# gwvault -`ansible-vault` CLI reimplemented in go +# gwvault - GoodwayGroup Ansible Vault + +> `ansible-vault` CLI reimplemented in go + +`ansible-vault` is a very powerful tool and we wanted to simplifying the install and management of the tool as a standalone, cross platform tool. + +## Basic Usage + +Us in place of `ansible-vault`. All commands are reimplemented except for `encrypt_string` and `rekey` (coming soom!). The tool will default to asking for your Vault password. + +``` +NAME: + gwvault - encryption/decryption utility for Ansible data files + +USAGE: + gwvault [global options] command [command options] [arguments...] + +COMMANDS: + encrypt encrypt file + decrypt decrypt file + edit edit file and re-encrypt + create create a new encypted file + view view contents of encrypted file + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --vault-password-file VAULT_PASSWORD_FILE vault password file VAULT_PASSWORD_FILE + --help, -h show help + --version, -v print the version +``` + +### Prerequisites + +* go v1.10+ +* make +* [auto-changelog](https://www.npmjs.com/package/auto-changelog) (Optional. Used for managing releases) + +## Built With + +* go v1.10+ +* make +* [github.com/mitchellh/gox](https://github.com/mitchellh/gox) + +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. + +## Versioning + +We employ [auto-changelog](https://www.npmjs.com/package/auto-changelog) to manage the [CHANGELOG.md](CHANGELOG.md). For the versions available, see the [tags on this repository](https://github.com/GoodwayGroup/gwvault/tags). + +## Authors + +* **Derek Smith** - [@clok](https://github.com/clok) + +See also the list of [contributors](https://github.com/GoodwayGroup/gwvault/contributors) who participated in this project. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details + +## Acknowledgments + +* Thank you to [@pbthorste](https://github.com/pbthorste) for doing the heavy lifting on [avtool](https://github.com/pbthorste/avtool) + +## Sponsors + +[![goodwaygroup][goodwaygroup]](https://goodwaygroup.com) + +[goodwaygroup]: https://s3.amazonaws.com/gw-crs-assets/goodwaygroup/logos/ggLogo_sm.png "Goodway Group" diff --git a/cmd/gwvault/main.go b/cmd/gwvault/main.go new file mode 100644 index 0000000..7f64216 --- /dev/null +++ b/cmd/gwvault/main.go @@ -0,0 +1,436 @@ +package main + +import ( + "errors" + "io/ioutil" + "os" + "os/exec" + "strings" + "syscall" + "github.com/pbthorste/avtool" + "golang.org/x/crypto/ssh/terminal" + "gopkg.in/urfave/cli.v1" +) + +var ( + version string +) + +// OG: [create|decrypt|edit|encrypt|encrypt_string|rekey|view] [options] [vaultfile.yml] +// DONE: [create|decrypt|edit|encrypt|view] +// TODO: [encrypt_string|rekey] + +func main() { + app := cli.NewApp() + app.Name = "gwvault" + app.Version = version + app.Usage = "encryption/decryption utility for Ansible data files" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "vault-password-file", + Usage: "vault password file `VAULT_PASSWORD_FILE`", + }, + } + app.Commands = []cli.Command{ + { + Name: "encrypt", + Usage: "encrypt file", + UsageText: "[options] [vaultfile.yml]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "vault-password-file", + Usage: "vault password file `VAULT_PASSWORD_FILE`", + }, + }, + Action: func(c *cli.Context) error { + vaultPassword := c.String("vault-password-file") + // Validate CLI args + err := validateCommandArgs(c) + if err != nil { + return err + } + vaultFileName, err := validateAndGetVaultFile(c) + if err != nil { + return err + } + pw, err := retrieveVaultPassword(vaultPassword) + if err != nil { + return cli.NewExitError(err, 2) + } + + // Encrypt + result, err := avtool.EncryptFile(vaultFileName, pw) + if err != nil { + return cli.NewExitError(err, 2) + } + err = ioutil.WriteFile(vaultFileName, []byte(result), 0644) + if err != nil { + return cli.NewExitError(err, 2) + } + + println("Encryption successful") + + return nil + }, + }, + { + Name: "decrypt", + Usage: "decrypt file", + UsageText: "[options] [vaultfile.yml]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "vault-password-file", + Usage: "vault password file `VAULT_PASSWORD_FILE`", + }, + }, + Action: func(c *cli.Context) error { + vaultPassword := c.String("vault-password-file") + // Validate CLI args + err := validateCommandArgs(c) + if err != nil { + return err + } + vaultFileName, err := validateAndGetVaultFile(c) + if err != nil { + return err + } + + // Get Vault password + pw, err := retrieveVaultPassword(vaultPassword) + if err != nil { + return cli.NewExitError(err, 2) + } + + // Decrypt + result, err := avtool.DecryptFile(vaultFileName, pw) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Create a temp file + tempFile, err := ioutil.TempFile("", "vault") + if err != nil { + return cli.NewExitError(err, 1) + } + + // Write decrypted contents to temp file + err = ioutil.WriteFile(tempFile.Name(), []byte(result), 0644) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Move temp file to old file + err = os.Rename(tempFile.Name(), vaultFileName) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Close file + err = tempFile.Close() + if err != nil { + return cli.NewExitError(err, 1) + } + + println("Decryption successful") + + return nil + }, + }, + { + Name: "edit", + Usage: "edit file and re-encrypt", + UsageText: "[options] [vaultfile.yml]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "vault-password-file", + Usage: "vault password file `VAULT_PASSWORD_FILE`", + }, + }, + Action: func(c *cli.Context) error { + vaultPassword := c.String("vault-password-file") + // Validate CLI args + err := validateCommandArgs(c) + if err != nil { + return err + } + vaultFileName, err := validateAndGetVaultFile(c) + if err != nil { + return err + } + + // Get Vault password + pw, err := retrieveVaultPassword(vaultPassword) + if err != nil { + return cli.NewExitError(err, 2) + } + + // Decrypt + result, err := avtool.DecryptFile(vaultFileName, pw) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Create a new temp file + tempFile, err := ioutil.TempFile("", "vault") + if err != nil { + return cli.NewExitError(err, 1) + } + + // Write decrypted contents to temp file + err = ioutil.WriteFile(tempFile.Name(), []byte(result), 0644) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Open editor for modifications + cmd := exec.Command("vim", tempFile.Name()) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return cli.NewExitError(err, 2) + } + + // Encrypt temp file + result, err = avtool.EncryptFile(tempFile.Name(), pw) + if err != nil { + return cli.NewExitError(err, 1) + } + err = ioutil.WriteFile(tempFile.Name(), []byte(result), 0644) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Move temp file to old file + err = os.Rename(tempFile.Name(), vaultFileName) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Close file + err = tempFile.Close() + if err != nil { + return cli.NewExitError(err, 1) + } + + println("Vault file edited") + + return nil + }, + }, + { + Name: "create", + Usage: "create a new encypted file", + UsageText: "[options] [vaultfile.yml]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "vault-password-file", + Usage: "vault password file `VAULT_PASSWORD_FILE`", + }, + }, + Action: func(c *cli.Context) error { + vaultPassword := c.String("vault-password-file") + // Validate CLI args + err := validateCommandArgs(c) + if err != nil { + return err + } + vaultFileName, err := validateAndGetVaultFileToCreate(c) + if err != nil { + return err + } + + // Get Vault password + pw, err := retrieveVaultPassword(vaultPassword) + if err != nil { + return cli.NewExitError(err, 2) + } + + // Create a new temp file + tempFile, err := ioutil.TempFile("", "vault") + if err != nil { + return cli.NewExitError(err, 2) + } + + // Open temp file for edit + cmd := exec.Command("vim", tempFile.Name()) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return cli.NewExitError(err, 2) + } + + // Encrypt temp file + result, err := avtool.EncryptFile(tempFile.Name(), pw) + if err != nil { + return cli.NewExitError(err, 2) + } + + // Write encrypted content to new file location + err = ioutil.WriteFile(vaultFileName, []byte(result), 0644) + if err != nil { + return cli.NewExitError(err, 2) + } + + // Close temp file + err = tempFile.Close() + if err != nil { + return cli.NewExitError(err, 2) + } + + // Delete the temp file + err = os.Remove(tempFile.Name()) + if err != nil { + return cli.NewExitError(err, 2) + } + + println("Vault file created") + + return nil + }, + }, + { + Name: "view", + Usage: "view contents of encrypted file", + UsageText: "[options] [vaultfile.yml]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "vault-password-file", + Usage: "vault password file `VAULT_PASSWORD_FILE`", + }, + }, + Action: func(c *cli.Context) error { + vaultPassword := c.String("vault-password-file") + // Validate CLI args + err := validateCommandArgs(c) + if err != nil { + return err + } + vaultFileName, err := validateAndGetVaultFile(c) + if err != nil { + return err + } + + // Get Vault password + pw, err := retrieveVaultPassword(vaultPassword) + if err != nil { + return cli.NewExitError(err, 2) + } + result, err := avtool.DecryptFile(vaultFileName, pw) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Create a new temp file + tempFile, err := ioutil.TempFile("", "vault") + if err != nil { + return cli.NewExitError(err, 1) + } + + err = ioutil.WriteFile(tempFile.Name(), []byte(result), 0644) + if err != nil { + return cli.NewExitError(err, 1) + } + + // Open 'more' stream of contents + cmd := exec.Command("more", tempFile.Name()) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Run() + + // Close temp file + err = tempFile.Close() + if err != nil { + return cli.NewExitError(err, 1) + } + + // Delete the temp file + err = os.Remove(tempFile.Name()) + if err != nil { + return cli.NewExitError(err, 1) + } + + return nil + }, + }, + } + app.Run(os.Args) +} + +func validateCommandArgs(c *cli.Context) (err error) { + if !c.Args().Present() { + cli.ShowSubcommandHelp(c) + return cli.NewExitError(errors.New("ERROR: Empty or Invalid inputs! Please ref. to usage instructions!"), 2) + } + return nil +} + +func validateAndGetVaultFile(c *cli.Context) (filename string, err error) { + filename = strings.TrimSpace(c.Args().First()) + if filename == "" { + cli.ShowSubcommandHelp(c) + return filename, cli.NewExitError(errors.New("ERROR: Filename not specified! Please ref. to usage instructions!"), 2) + } else { + if fileInfo, err := os.Stat(filename); os.IsNotExist(err) { + cli.ShowSubcommandHelp(c) + return filename, cli.NewExitError(errors.New("ERROR: file "+filename+" "+"doesn't exist!"), 2) + } else { + if fileInfo.IsDir() { + cli.ShowSubcommandHelp(c) + return filename, cli.NewExitError(errors.New("ERROR: file "+filename+" is a "+"directory!"), 2) + } + } + } + return filename, nil +} + +func validateAndGetVaultFileToCreate(c *cli.Context) (filename string, err error) { + filename = strings.TrimSpace(c.Args().First()) + if filename == "" { + cli.ShowSubcommandHelp(c) + return filename, cli.NewExitError(errors.New("ERROR: Filename not specified! Please ref. to usage instructions!"), 2) + } else { + if fileInfo, err := os.Stat(filename); os.IsNotExist(err) { + // File does not exist, good to go + return filename, nil + } else { + if fileInfo.IsDir() { + cli.ShowSubcommandHelp(c) + return filename, cli.NewExitError(errors.New("ERROR: file "+filename+" is a directory!"), 2) + } + return filename, cli.NewExitError(errors.New("ERROR: file "+filename+" already exists!"), 2) + } + } + // return filename, error on error; nil if no error; + return filename, nil +} + +func retrieveVaultPassword(vaultPasswordFile string) (string, error) { + if vaultPasswordFile != "" { + if _, err := os.Stat(vaultPasswordFile); os.IsNotExist(err) { + return "", errors.New("ERROR: vault-password-file, could not find: " + vaultPasswordFile) + } + pw, err := ioutil.ReadFile(vaultPasswordFile) + if err != nil { + return "", errors.New("ERROR: vault-password-file, " + err.Error()) + } + return strings.TrimSpace(string(pw)), nil + } + + return readVaultPassword() +} + +func readVaultPassword() (password string, err error) { + println("Vault password: ") + bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) + if err != nil { + err = errors.New("ERROR: could not input password, " + err.Error()) + return + } + password = string(bytePassword) + return +}