mirror of
https://github.com/getsops/sops.git
synced 2026-02-05 12:45:21 +01:00
@@ -19,6 +19,11 @@ type encryptedValue struct {
|
||||
|
||||
const nonceSize int = 32
|
||||
|
||||
type stashData struct {
|
||||
iv []byte
|
||||
plaintext interface{}
|
||||
}
|
||||
|
||||
// Cipher encrypts and decrypts data keys with AES GCM 256
|
||||
type Cipher struct{}
|
||||
|
||||
@@ -46,54 +51,67 @@ func parse(value string) (*encryptedValue, error) {
|
||||
return &encryptedValue{data, iv, tag, datatype}, nil
|
||||
}
|
||||
|
||||
// Decrypt takes a sops-format value string and a key and returns the decrypted value.
|
||||
func (c Cipher) Decrypt(value string, key []byte, additionalAuthData []byte) (interface{}, error) {
|
||||
// Decrypt takes a sops-format value string and a key and returns the decrypted value and a stash value
|
||||
func (c Cipher) Decrypt(value string, key []byte, path string) (plaintext interface{}, stash interface{}, err error) {
|
||||
encryptedValue, err := parse(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
aescipher, err := cryptoaes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCMWithNonceSize(aescipher, len(encryptedValue.iv))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
stashValue := stashData{iv: encryptedValue.iv}
|
||||
data := append(encryptedValue.data, encryptedValue.tag...)
|
||||
decryptedBytes, err := gcm.Open(nil, encryptedValue.iv, data, additionalAuthData)
|
||||
decryptedBytes, err := gcm.Open(nil, encryptedValue.iv, data, []byte(path))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not decrypt with AES_GCM: %s", err)
|
||||
return "", nil, fmt.Errorf("Could not decrypt with AES_GCM: %s", err)
|
||||
}
|
||||
decryptedValue := string(decryptedBytes)
|
||||
switch encryptedValue.datatype {
|
||||
case "str":
|
||||
return decryptedValue, nil
|
||||
stashValue.plaintext = decryptedValue
|
||||
return decryptedValue, stashValue, nil
|
||||
case "int":
|
||||
return strconv.Atoi(decryptedValue)
|
||||
plaintext, err = strconv.Atoi(decryptedValue)
|
||||
stashValue.plaintext = plaintext
|
||||
return plaintext, stashValue, err
|
||||
case "float":
|
||||
return strconv.ParseFloat(decryptedValue, 64)
|
||||
plaintext, err = strconv.ParseFloat(decryptedValue, 64)
|
||||
stashValue.plaintext = plaintext
|
||||
return plaintext, stashValue, err
|
||||
case "bytes":
|
||||
return decryptedValue, nil
|
||||
stashValue.plaintext = decryptedBytes
|
||||
return decryptedBytes, stashValue, nil
|
||||
case "bool":
|
||||
return strconv.ParseBool(decryptedValue)
|
||||
plaintext, err = strconv.ParseBool(decryptedValue)
|
||||
stashValue.plaintext = plaintext
|
||||
return plaintext, stashValue, err
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown datatype: %s", encryptedValue.datatype)
|
||||
return nil, nil, fmt.Errorf("Unknown datatype: %s", encryptedValue.datatype)
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt takes one of (string, int, float, bool) and encrypts it with the provided key and additional auth data, returning a sops-format encrypted string.
|
||||
func (c Cipher) Encrypt(value interface{}, key []byte, additionalAuthData []byte) (string, error) {
|
||||
func (c Cipher) Encrypt(value interface{}, key []byte, path string, stash interface{}) (string, error) {
|
||||
aescipher, err := cryptoaes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not initialize AES GCM encryption cipher: %s", err)
|
||||
}
|
||||
iv := make([]byte, nonceSize)
|
||||
_, err = rand.Read(iv)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not generate random bytes for IV: %s", err)
|
||||
var iv []byte
|
||||
if stash, ok := stash.(stashData); !ok || stash.plaintext != value {
|
||||
iv = make([]byte, nonceSize)
|
||||
_, err = rand.Read(iv)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not generate random bytes for IV: %s", err)
|
||||
}
|
||||
} else {
|
||||
iv = stash.iv
|
||||
}
|
||||
gcm, err := cipher.NewGCMWithNonceSize(aescipher, nonceSize)
|
||||
if err != nil {
|
||||
@@ -117,7 +135,7 @@ func (c Cipher) Encrypt(value interface{}, key []byte, additionalAuthData []byte
|
||||
default:
|
||||
return "", fmt.Errorf("Value to encrypt has unsupported type %T", value)
|
||||
}
|
||||
out := gcm.Seal(nil, iv, plaintext, additionalAuthData)
|
||||
out := gcm.Seal(nil, iv, plaintext, []byte(path))
|
||||
return fmt.Sprintf("ENC[AES256_GCM,data:%s,iv:%s,tag:%s,type:%s]",
|
||||
base64.StdEncoding.EncodeToString(out[:len(out)-cryptoaes.BlockSize]),
|
||||
base64.StdEncoding.EncodeToString(iv),
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestDecrypt(t *testing.T) {
|
||||
expected := "foo"
|
||||
key := []byte(strings.Repeat("f", 32))
|
||||
message := `ENC[AES256_GCM,data:oYyi,iv:MyIDYbT718JRr11QtBkcj3Dwm4k1aCGZBVeZf0EyV8o=,tag:t5z2Z023Up0kxwCgw1gNxg==,type:str]`
|
||||
decryption, err := Cipher{}.Decrypt(message, key, []byte("bar:"))
|
||||
decryption, _, err := Cipher{}.Decrypt(message, key, "bar:")
|
||||
if err != nil {
|
||||
t.Errorf("%s", err)
|
||||
}
|
||||
@@ -23,25 +23,25 @@ func TestDecrypt(t *testing.T) {
|
||||
|
||||
func TestDecryptInvalidAad(t *testing.T) {
|
||||
message := `ENC[AES256_GCM,data:oYyi,iv:MyIDYbT718JRr11QtBkcj3Dwm4k1aCGZBVeZf0EyV8o=,tag:t5z2Z023Up0kxwCgw1gNxg==,type:str]`
|
||||
_, err := Cipher{}.Decrypt(message, []byte(strings.Repeat("f", 32)), []byte(""))
|
||||
_, _, err := Cipher{}.Decrypt(message, []byte(strings.Repeat("f", 32)), "")
|
||||
if err == nil {
|
||||
t.Errorf("Decrypting with an invalid AAC should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundtripString(t *testing.T) {
|
||||
f := func(x string, aad []byte) bool {
|
||||
f := func(x, aad string) bool {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
if x == "" {
|
||||
return true
|
||||
}
|
||||
s, err := Cipher{}.Encrypt(x, key, aad)
|
||||
s, err := Cipher{}.Encrypt(x, key, aad, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
d, err := Cipher{}.Decrypt(s, key, aad)
|
||||
d, _, err := Cipher{}.Decrypt(s, key, aad)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -55,12 +55,12 @@ func TestRoundtripString(t *testing.T) {
|
||||
func TestRoundtripFloat(t *testing.T) {
|
||||
key := []byte(strings.Repeat("f", 32))
|
||||
f := func(x float64) bool {
|
||||
s, err := Cipher{}.Encrypt(x, key, []byte(""))
|
||||
s, err := Cipher{}.Encrypt(x, key, "", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
d, err := Cipher{}.Decrypt(s, key, []byte(""))
|
||||
d, _, err := Cipher{}.Decrypt(s, key, "")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -74,12 +74,12 @@ func TestRoundtripFloat(t *testing.T) {
|
||||
func TestRoundtripInt(t *testing.T) {
|
||||
key := []byte(strings.Repeat("f", 32))
|
||||
f := func(x int) bool {
|
||||
s, err := Cipher{}.Encrypt(x, key, []byte(""))
|
||||
s, err := Cipher{}.Encrypt(x, key, "", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
d, err := Cipher{}.Decrypt(s, key, []byte(""))
|
||||
d, _, err := Cipher{}.Decrypt(s, key, "")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -93,12 +93,12 @@ func TestRoundtripInt(t *testing.T) {
|
||||
func TestRoundtripBool(t *testing.T) {
|
||||
key := []byte(strings.Repeat("f", 32))
|
||||
f := func(x bool) bool {
|
||||
s, err := Cipher{}.Encrypt(x, key, []byte(""))
|
||||
s, err := Cipher{}.Encrypt(x, key, "", nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
d, err := Cipher{}.Decrypt(s, key, []byte(""))
|
||||
d, _, err := Cipher{}.Decrypt(s, key, "")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
245
cmd/sops/main.go
245
cmd/sops/main.go
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"go.mozilla.org/sops"
|
||||
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"go.mozilla.org/sops/aes"
|
||||
"go.mozilla.org/sops/json"
|
||||
@@ -14,6 +17,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -141,7 +145,7 @@ func main() {
|
||||
output = os.Stdout
|
||||
}
|
||||
if err != nil {
|
||||
return cli.NewExitError("Error: could not read file", exitCouldNotReadInputFile)
|
||||
fileBytes = nil
|
||||
}
|
||||
if c.Bool("encrypt") {
|
||||
return encrypt(c, file, fileBytes, output)
|
||||
@@ -150,12 +154,12 @@ func main() {
|
||||
} else if c.Bool("rotate") {
|
||||
return rotate(c, file, fileBytes, output)
|
||||
}
|
||||
return nil
|
||||
return edit(c, file, fileBytes)
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func runEditor(path string) {
|
||||
func runEditor(path string) error {
|
||||
editor := os.Getenv("EDITOR")
|
||||
var cmd *exec.Cmd
|
||||
if editor == "" {
|
||||
@@ -168,7 +172,11 @@ func runEditor(path string) {
|
||||
} else {
|
||||
cmd = exec.Command(editor, path)
|
||||
}
|
||||
cmd.Run()
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func store(path string) sops.Store {
|
||||
@@ -180,36 +188,36 @@ func store(path string) sops.Store {
|
||||
panic("Unknown file type for file " + path)
|
||||
}
|
||||
|
||||
func decryptFile(store sops.Store, fileBytes []byte, ignoreMac bool) (sops.Tree, error) {
|
||||
var tree sops.Tree
|
||||
func decryptFile(store sops.Store, fileBytes []byte, ignoreMac bool) (tree sops.Tree, stash map[string][]interface{}, err error) {
|
||||
metadata, err := store.UnmarshalMetadata(fileBytes)
|
||||
if err != nil {
|
||||
return tree, cli.NewExitError(fmt.Sprintf("Error loading file: %s", err), exitCouldNotReadInputFile)
|
||||
return tree, nil, cli.NewExitError(fmt.Sprintf("Error loading file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
key, err := metadata.GetDataKey()
|
||||
if err != nil {
|
||||
return tree, cli.NewExitError(err.Error(), exitCouldNotRetrieveKey)
|
||||
return tree, nil, cli.NewExitError(err.Error(), exitCouldNotRetrieveKey)
|
||||
}
|
||||
branch, err := store.Unmarshal(fileBytes)
|
||||
if err != nil {
|
||||
return tree, cli.NewExitError(fmt.Sprintf("Error loading file: %s", err), exitCouldNotReadInputFile)
|
||||
return tree, nil, cli.NewExitError(fmt.Sprintf("Error loading file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
tree = sops.Tree{Branch: branch, Metadata: metadata}
|
||||
cipher := aes.Cipher{}
|
||||
mac, err := tree.Decrypt(key, cipher)
|
||||
stash = make(map[string][]interface{})
|
||||
mac, err := tree.Decrypt(key, cipher, stash)
|
||||
if err != nil {
|
||||
return tree, cli.NewExitError(fmt.Sprintf("Error decrypting tree: %s", err), exitErrorDecryptingTree)
|
||||
return tree, nil, cli.NewExitError(fmt.Sprintf("Error decrypting tree: %s", err), exitErrorDecryptingTree)
|
||||
}
|
||||
originalMac, err := cipher.Decrypt(metadata.MessageAuthenticationCode, key, []byte(metadata.LastModified.Format(time.RFC3339)))
|
||||
originalMac, _, err := cipher.Decrypt(metadata.MessageAuthenticationCode, key, metadata.LastModified.Format(time.RFC3339))
|
||||
if originalMac != mac && !ignoreMac {
|
||||
return tree, cli.NewExitError(fmt.Sprintf("MAC mismatch. File has %s, computed %s", originalMac, mac), 9)
|
||||
return tree, nil, cli.NewExitError(fmt.Sprintf("MAC mismatch. File has %s, computed %s", originalMac, mac), 9)
|
||||
}
|
||||
return tree, nil
|
||||
return tree, stash, nil
|
||||
}
|
||||
|
||||
func decrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) error {
|
||||
store := store(file)
|
||||
tree, err := decryptFile(store, fileBytes, c.Bool("ignore-mac"))
|
||||
tree, _, err := decryptFile(store, fileBytes, c.Bool("ignore-mac"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -240,15 +248,7 @@ func decrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func encrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) error {
|
||||
store := store(file)
|
||||
branch, err := store.Unmarshal(fileBytes)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Error loading file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
var metadata sops.Metadata
|
||||
metadata.UnencryptedSuffix = c.String("unencrypted-suffix")
|
||||
metadata.Version = "2.0.0"
|
||||
func getKeysources(c *cli.Context, file string) ([]sops.KeySource, error) {
|
||||
var kmsKeys []sops.MasterKey
|
||||
var pgpKeys []sops.MasterKey
|
||||
|
||||
@@ -262,13 +262,13 @@ func encrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) er
|
||||
pgpKeys = append(pgpKeys, &k)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if c.String("kms") == "" && c.String("pgp") == "" {
|
||||
var confBytes []byte
|
||||
if c.String("config") != "" {
|
||||
confBytes, err = ioutil.ReadFile(c.String("config"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Error loading config file: %s", err), exitErrorReadingConfig)
|
||||
return nil, cli.NewExitError(fmt.Sprintf("Error loading config file: %s", err), exitErrorReadingConfig)
|
||||
}
|
||||
}
|
||||
kmsString, pgpString, err := yaml.MasterKeyStringsForFile(file, confBytes)
|
||||
@@ -283,16 +283,32 @@ func encrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) er
|
||||
}
|
||||
kmsKs := sops.KeySource{Name: "kms", Keys: kmsKeys}
|
||||
pgpKs := sops.KeySource{Name: "pgp", Keys: pgpKeys}
|
||||
metadata.KeySources = append(metadata.KeySources, kmsKs)
|
||||
metadata.KeySources = append(metadata.KeySources, pgpKs)
|
||||
return []sops.KeySource{kmsKs, pgpKs}, nil
|
||||
}
|
||||
|
||||
func encrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) error {
|
||||
store := store(file)
|
||||
branch, err := store.Unmarshal(fileBytes)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Error loading file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
ks, err := getKeysources(c, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata := sops.Metadata{
|
||||
UnencryptedSuffix: c.String("unencrypted-suffix"),
|
||||
Version: "2.0.0",
|
||||
KeySources: ks,
|
||||
}
|
||||
tree := sops.Tree{Branch: branch, Metadata: metadata}
|
||||
key, err := tree.GenerateDataKey()
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), exitCouldNotRetrieveKey)
|
||||
}
|
||||
cipher := aes.Cipher{}
|
||||
mac, err := tree.Encrypt(key, cipher)
|
||||
encryptedMac, err := cipher.Encrypt(mac, key, []byte(metadata.LastModified.Format(time.RFC3339)))
|
||||
mac, err := tree.Encrypt(key, cipher, nil)
|
||||
encryptedMac, err := cipher.Encrypt(mac, key, metadata.LastModified.Format(time.RFC3339), nil)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not encrypt MAC: %s", err), exitErrorEncryptingTree)
|
||||
}
|
||||
@@ -307,7 +323,7 @@ func encrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) er
|
||||
|
||||
func rotate(c *cli.Context, file string, fileBytes []byte, output io.Writer) error {
|
||||
store := store(file)
|
||||
tree, err := decryptFile(store, fileBytes, c.Bool("ignore-mac"))
|
||||
tree, _, err := decryptFile(store, fileBytes, c.Bool("ignore-mac"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -316,7 +332,7 @@ func rotate(c *cli.Context, file string, fileBytes []byte, output io.Writer) err
|
||||
return cli.NewExitError(err.Error(), exitCouldNotRetrieveKey)
|
||||
}
|
||||
cipher := aes.Cipher{}
|
||||
_, err = tree.Encrypt(newKey, cipher)
|
||||
_, err = tree.Encrypt(newKey, cipher, nil)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Error encrypting tree: %s", err), exitErrorEncryptingTree)
|
||||
}
|
||||
@@ -333,3 +349,168 @@ func rotate(c *cli.Context, file string, fileBytes []byte, output io.Writer) err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashFile(filePath string) ([]byte, error) {
|
||||
var result []byte
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer file.Close()
|
||||
hash := md5.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return result, err
|
||||
}
|
||||
return hash.Sum(result), nil
|
||||
}
|
||||
|
||||
const exampleYaml = `
|
||||
# Welcome to SOPS. This is the default template.
|
||||
# Remove these lines and add your data.
|
||||
# Don't modify the 'sops' section, it contains key material.
|
||||
example_key: example_value
|
||||
example_array:
|
||||
- example_value1
|
||||
- example_value2
|
||||
example_multiline: |
|
||||
this is a
|
||||
multiline
|
||||
entry
|
||||
example_number: 1234.5678
|
||||
example:
|
||||
nested:
|
||||
values: delete_me
|
||||
example_booleans:
|
||||
- true
|
||||
- false
|
||||
`
|
||||
|
||||
func loadExample(c *cli.Context, file string) (sops.Tree, error) {
|
||||
var in []byte
|
||||
var tree sops.Tree
|
||||
if strings.HasSuffix(file, ".yaml") {
|
||||
in = []byte(exampleYaml)
|
||||
}
|
||||
branch, _ := store(file).Unmarshal(in)
|
||||
tree.Branch = branch
|
||||
ks, err := getKeysources(c, file)
|
||||
if err != nil {
|
||||
return tree, err
|
||||
}
|
||||
tree.Metadata.UnencryptedSuffix = c.String("unencrypted-suffix")
|
||||
tree.Metadata.Version = "2.0.0"
|
||||
tree.Metadata.KeySources = ks
|
||||
key, err := tree.GenerateDataKey()
|
||||
if err != nil {
|
||||
return tree, cli.NewExitError(err.Error(), exitCouldNotRetrieveKey)
|
||||
}
|
||||
tree.Metadata.UpdateMasterKeys(key)
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
func edit(c *cli.Context, file string, fileBytes []byte) error {
|
||||
var tree sops.Tree
|
||||
var stash map[string][]interface{}
|
||||
var err error
|
||||
if fileBytes == nil {
|
||||
tree, err = loadExample(c, file)
|
||||
} else {
|
||||
tree, stash, err = decryptFile(store(file), fileBytes, c.Bool("ignore-mac"))
|
||||
}
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not load file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
tmpdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not create temporary directory: %s", err), exitCouldNotWriteOutputFile)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
tmpfile, err := os.Create(path.Join(tmpdir, path.Base(file)))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not create temporary file: %s", err), exitCouldNotWriteOutputFile)
|
||||
}
|
||||
var out []byte
|
||||
if c.Bool("show-master-keys") {
|
||||
out, err = store(file).MarshalWithMetadata(tree.Branch, tree.Metadata)
|
||||
} else {
|
||||
out, err = store(file).Marshal(tree.Branch)
|
||||
}
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), exitErrorDumpingTree)
|
||||
}
|
||||
_, err = tmpfile.Write(out)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not write output file: %s", err), exitCouldNotWriteOutputFile)
|
||||
}
|
||||
origHash, err := hashFile(tmpfile.Name())
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not hash file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
for {
|
||||
err = runEditor(tmpfile.Name())
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not run editor: %s", err), exitNoEditorFound)
|
||||
}
|
||||
newHash, err := hashFile(tmpfile.Name())
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not hash file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
if bytes.Equal(newHash, origHash) {
|
||||
return cli.NewExitError("File has not changed, exiting.", exitFileHasNotBeenModified)
|
||||
}
|
||||
edited, err := ioutil.ReadFile(tmpfile.Name())
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not read edited file: %s", err), exitCouldNotReadInputFile)
|
||||
}
|
||||
newBranch, err := store(file).Unmarshal(edited)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not load tree: %s\nProbably invalid syntax. Press a key to return to the editor, or Ctrl+C to exit.", err)
|
||||
bufio.NewReader(os.Stdin).ReadByte()
|
||||
continue
|
||||
}
|
||||
if c.Bool("show-master-keys") {
|
||||
metadata, err := store(file).UnmarshalMetadata(edited)
|
||||
if err != nil {
|
||||
fmt.Printf("sops branch is invalid: %s.\nPress a key to return to the editor, or Ctrl+C to exit.", err)
|
||||
bufio.NewReader(os.Stdin).ReadByte()
|
||||
continue
|
||||
}
|
||||
tree.Metadata = metadata
|
||||
}
|
||||
tree.Branch = newBranch
|
||||
if tree.Metadata.MasterKeyCount() == 0 {
|
||||
fmt.Println("No master keys were provided, so sops can't encrypt the file.\nPress a key to return to the editor, or Ctrl+C to exit.")
|
||||
bufio.NewReader(os.Stdin).ReadByte()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
key, err := tree.Metadata.GetDataKey()
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not retrieve data key: %s", err), exitCouldNotRetrieveKey)
|
||||
}
|
||||
cipher := aes.Cipher{}
|
||||
mac, err := tree.Encrypt(key, cipher, stash)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not encrypt tree: %s", err), exitErrorEncryptingTree)
|
||||
}
|
||||
encryptedMac, err := cipher.Encrypt(mac, key, tree.Metadata.LastModified.Format(time.RFC3339), stash)
|
||||
if err != nil {
|
||||
|
||||
return cli.NewExitError(fmt.Sprintf("Could not encrypt MAC: %s", err), exitErrorEncryptingTree)
|
||||
}
|
||||
tree.Metadata.MessageAuthenticationCode = encryptedMac
|
||||
out, err = store(file).MarshalWithMetadata(tree.Branch, tree.Metadata)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), exitErrorDumpingTree)
|
||||
}
|
||||
output, err := os.Create(file)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), exitCouldNotWriteOutputFile)
|
||||
}
|
||||
_, err = output.Write(out)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("Could not write output file: %s", err), exitCouldNotWriteOutputFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
23
sops.go
23
sops.go
@@ -27,8 +27,8 @@ const MetadataNotFound = sopsError("sops metadata not found")
|
||||
|
||||
// DataKeyCipher provides a way to encrypt and decrypt the data key used to encrypt and decrypt sops files, so that the data key can be stored alongside the encrypted content. A DataKeyCipher must be able to decrypt the values it encrypts.
|
||||
type DataKeyCipher interface {
|
||||
Encrypt(value interface{}, key []byte, additionalAuthData []byte) (string, error)
|
||||
Decrypt(value string, key []byte, additionalAuthData []byte) (interface{}, error)
|
||||
Encrypt(value interface{}, key []byte, path string, stash interface{}) (string, error)
|
||||
Decrypt(value string, key []byte, path string) (plaintext interface{}, stashValue interface{}, err error)
|
||||
}
|
||||
|
||||
// TreeItem is an item inside sops's tree
|
||||
@@ -118,13 +118,21 @@ func (tree TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(in
|
||||
}
|
||||
|
||||
// Encrypt walks over the tree and encrypts all values with the provided cipher, except those whose key ends with the UnencryptedSuffix specified on the Metadata struct. If encryption is successful, it returns the MAC for the encrypted tree.
|
||||
func (tree Tree) Encrypt(key []byte, cipher DataKeyCipher) (string, error) {
|
||||
func (tree Tree) Encrypt(key []byte, cipher DataKeyCipher, stash map[string][]interface{}) (string, error) {
|
||||
hash := sha512.New()
|
||||
_, err := tree.Branch.walkBranch(tree.Branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
|
||||
bytes, err := ToBytes(in)
|
||||
if !strings.HasSuffix(path[len(path)-1], tree.Metadata.UnencryptedSuffix) {
|
||||
var err error
|
||||
in, err = cipher.Encrypt(in, key, []byte(strings.Join(path, ":")+":"))
|
||||
pathString := strings.Join(path, ":") + ":"
|
||||
// Pop from the left of the stash
|
||||
var stashValue interface{}
|
||||
if len(stash[pathString]) > 0 {
|
||||
var newStash []interface{}
|
||||
stashValue, newStash = stash[pathString][0], stash[pathString][1:len(stash[pathString])]
|
||||
stash[pathString] = newStash
|
||||
}
|
||||
in, err = cipher.Encrypt(in, key, pathString, stashValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not encrypt value: %s", err)
|
||||
}
|
||||
@@ -142,16 +150,19 @@ func (tree Tree) Encrypt(key []byte, cipher DataKeyCipher) (string, error) {
|
||||
}
|
||||
|
||||
// Decrypt walks over the tree and decrypts all values with the provided cipher, except those whose key ends with the UnencryptedSuffix specified on the Metadata struct. If decryption is successful, it returns the MAC for the decrypted tree.
|
||||
func (tree Tree) Decrypt(key []byte, cipher DataKeyCipher) (string, error) {
|
||||
func (tree Tree) Decrypt(key []byte, cipher DataKeyCipher, stash map[string][]interface{}) (string, error) {
|
||||
hash := sha512.New()
|
||||
_, err := tree.Branch.walkBranch(tree.Branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) {
|
||||
var v interface{}
|
||||
if !strings.HasSuffix(path[len(path)-1], tree.Metadata.UnencryptedSuffix) {
|
||||
var err error
|
||||
v, err = cipher.Decrypt(in.(string), key, []byte(strings.Join(path, ":")+":"))
|
||||
var stashValue interface{}
|
||||
pathString := strings.Join(path, ":") + ":"
|
||||
v, stashValue, err = cipher.Decrypt(in.(string), key, pathString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not decrypt value: %s", err)
|
||||
}
|
||||
stash[pathString] = append(stash[pathString], stashValue)
|
||||
} else {
|
||||
v = in
|
||||
}
|
||||
|
||||
14
sops_test.go
14
sops_test.go
@@ -23,14 +23,14 @@ func TestUnencryptedSuffix(t *testing.T) {
|
||||
},
|
||||
}
|
||||
cipher := aes.Cipher{}
|
||||
_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
|
||||
_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Encrypting the tree failed: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(tree.Branch, expected) {
|
||||
t.Errorf("Trees don't match: \ngot \t\t%+v,\n expected \t\t%+v", tree.Branch, expected)
|
||||
}
|
||||
_, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher)
|
||||
_, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Decrypting the tree failed: %s", err)
|
||||
}
|
||||
@@ -41,12 +41,12 @@ func TestUnencryptedSuffix(t *testing.T) {
|
||||
|
||||
type MockCipher struct{}
|
||||
|
||||
func (m MockCipher) Encrypt(value interface{}, key []byte, additionalAuthData []byte) (string, error) {
|
||||
func (m MockCipher) Encrypt(value interface{}, key []byte, path string, stashValue interface{}) (string, error) {
|
||||
return "a", nil
|
||||
}
|
||||
|
||||
func (m MockCipher) Decrypt(value string, key []byte, additionalAuthData []byte) (interface{}, error) {
|
||||
return "a", nil
|
||||
func (m MockCipher) Decrypt(value string, key []byte, path string) (interface{}, interface{}, error) {
|
||||
return "a", nil, nil
|
||||
}
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
@@ -97,7 +97,7 @@ func TestEncrypt(t *testing.T) {
|
||||
},
|
||||
}
|
||||
tree := Tree{Branch: branch, Metadata: Metadata{UnencryptedSuffix: DefaultUnencryptedSuffix}}
|
||||
tree.Encrypt(bytes.Repeat([]byte{'f'}, 32), MockCipher{})
|
||||
tree.Encrypt(bytes.Repeat([]byte{'f'}, 32), MockCipher{}, make(map[string][]interface{}))
|
||||
if !reflect.DeepEqual(tree.Branch, expected) {
|
||||
t.Errorf("%s does not equal expected tree: %s", tree.Branch, expected)
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func TestDecrypt(t *testing.T) {
|
||||
},
|
||||
}
|
||||
tree := Tree{Branch: branch, Metadata: Metadata{UnencryptedSuffix: DefaultUnencryptedSuffix}}
|
||||
tree.Decrypt(bytes.Repeat([]byte{'f'}, 32), MockCipher{})
|
||||
tree.Decrypt(bytes.Repeat([]byte{'f'}, 32), MockCipher{}, make(map[string][]interface{}))
|
||||
if !reflect.DeepEqual(tree.Branch, expected) {
|
||||
t.Errorf("%s does not equal expected tree: %s", tree.Branch, expected)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user