Initial commit
This commit is contained in:
commit
c99ba53f8e
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
unseal
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Kevin Cotugno
|
||||
|
||||
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.
|
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Unseal
|
||||
|
||||
A command line program that wraps runs another command line program with
|
||||
additional environment variables which are stored in a GPG encrypted file.
|
300
unseal.go
Normal file
300
unseal.go
Normal file
@ -0,0 +1,300 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var help bool
|
||||
var cmd string
|
||||
var group string
|
||||
var execargs []string
|
||||
|
||||
var secretsFile string
|
||||
|
||||
const mode = 0600
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&help, "help", false, "Show this usage message")
|
||||
flag.StringVar(&cmd, "cmd", "wrap", "Command to run\nValid commands:\n\tdecrypt\n\tedit\n\twrap\n")
|
||||
flag.StringVar(&group, "group", "", "Secrets group to execute on")
|
||||
flag.Parse()
|
||||
|
||||
execargs = flag.Args()
|
||||
secretsFile = fmt.Sprintf("%s/.secrets/%s.gpg", os.Getenv("HOME"), group)
|
||||
}
|
||||
|
||||
func system(command string, pipe bool, args ...string) (string, string, error) {
|
||||
var err error
|
||||
var stdout, stderr []byte
|
||||
var stdoutPipe, stderrPipe io.ReadCloser
|
||||
|
||||
c := exec.Command(command, args...)
|
||||
|
||||
c.Stdin = os.Stdin
|
||||
if pipe {
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
} else {
|
||||
stdoutPipe, err = c.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer stdoutPipe.Close()
|
||||
|
||||
stderrPipe, err = c.StderrPipe()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer stderrPipe.Close()
|
||||
}
|
||||
|
||||
err = c.Start()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if !pipe {
|
||||
stdout, err = ioutil.ReadAll(stdoutPipe)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
stderr, err = ioutil.ReadAll(stderrPipe)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = c.Wait()
|
||||
|
||||
return string(stdout), string(stderr), err
|
||||
}
|
||||
|
||||
func gpg(args ...string) (string, string, error) {
|
||||
return system("gpg", false, append([]string{"--quiet", "--no-verbose"}, args...)...)
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
if help {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "decrypt":
|
||||
fmt.Println(decrypt())
|
||||
case "edit":
|
||||
edit()
|
||||
case "wrap":
|
||||
wrap()
|
||||
default:
|
||||
fmt.Println("Unknown command: ", cmd)
|
||||
printHelp()
|
||||
}
|
||||
}
|
||||
|
||||
func ensureSecrets() {
|
||||
ensureGroup()
|
||||
|
||||
if !fileExists(secretsFile) {
|
||||
fmt.Println("Secrets file ", group, "does not exist. Create one with the edit command")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureGroup() {
|
||||
if group == "" {
|
||||
fmt.Println("Group name is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func decryptFile() string {
|
||||
if !fileExists(secretsFile) {
|
||||
return ""
|
||||
}
|
||||
|
||||
stdout, stderr, err := gpg("-d", secretsFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err, "\n", stderr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout)
|
||||
}
|
||||
|
||||
func decrypt() string {
|
||||
ensureSecrets()
|
||||
|
||||
return decryptFile()
|
||||
}
|
||||
|
||||
func edit() {
|
||||
var contents string
|
||||
ensureGroup()
|
||||
|
||||
if fileExists(secretsFile) {
|
||||
contents = decryptFile()
|
||||
}
|
||||
|
||||
file, err := writeTmpFile(contents)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening temporary file")
|
||||
os.Exit(1)
|
||||
}
|
||||
cleanup := func() {
|
||||
file.Close()
|
||||
err := os.Remove(file.Name())
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error cleaning up temp file. Unencrypted secrets may have leaked ", err)
|
||||
}
|
||||
}
|
||||
|
||||
tmpEnc := fmt.Sprintf("%s.gpg", file.Name())
|
||||
|
||||
err = editFile(file.Name())
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error editing secrets file: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, stderr, err := gpg("--armor", "--cipher-algo", "AES256", "-c", "-o", tmpEnc, file.Name())
|
||||
cleanup()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error encrypting temporary file: ", err, "\n", stderr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = copyFile(tmpEnc, secretsFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to move encrypted temp file to secrets dir: ", err)
|
||||
cleanup()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func wrap() {
|
||||
if len(execargs) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Wrap requires at least an external program to run")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ensureSecrets()
|
||||
|
||||
insertEnvironment(parseEnvironment(decrypt()))
|
||||
|
||||
_, _, err := system(execargs[0], true, execargs[1:len(execargs)]...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error executing external command: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTmpFile(contents string) (*os.File, error) {
|
||||
tmpFile := filepath.Join(os.TempDir(), "unseal."+randChars())
|
||||
|
||||
f, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = f.Chmod(mode)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(contents)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, err
|
||||
}
|
||||
|
||||
func editFile(file string) error {
|
||||
editor := os.Getenv("EDITOR")
|
||||
if editor == "" {
|
||||
editor = "vi"
|
||||
}
|
||||
|
||||
_, _, err := system(editor, true, file)
|
||||
return err
|
||||
}
|
||||
|
||||
func copyFile(oldpath, newpath string) error {
|
||||
err := os.Rename(oldpath, newpath)
|
||||
if err != nil {
|
||||
byteArr, err2 := ioutil.ReadFile(oldpath)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
err2 = ioutil.WriteFile(newpath, byteArr, mode)
|
||||
if err2 == nil {
|
||||
_ = os.Remove(oldpath)
|
||||
} else {
|
||||
_ = os.Remove(newpath)
|
||||
}
|
||||
|
||||
return err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func insertEnvironment(vars map[string]string) {
|
||||
for key, val := range vars {
|
||||
os.Setenv(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnvironment(raw string) map[string]string {
|
||||
vars := make(map[string]string)
|
||||
|
||||
for _, v := range strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") {
|
||||
if v == strings.TrimSpace("") {
|
||||
continue
|
||||
}
|
||||
|
||||
splitVar := strings.Split(v, "=")
|
||||
if len(splitVar) > 1 {
|
||||
vars[splitVar[0]] = splitVar[1]
|
||||
}
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
func randChars() string {
|
||||
buf := make([]byte, 4)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to create temporary file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
flag.PrintDefaults()
|
||||
}
|
Loading…
Reference in New Issue
Block a user