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