diff --git a/aes/decryptor.go b/aes/decryptor.go index 707b764e5..25207cd0f 100644 --- a/aes/decryptor.go +++ b/aes/decryptor.go @@ -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), diff --git a/aes/decryptor_test.go b/aes/decryptor_test.go index e6e715565..899ceb211 100644 --- a/aes/decryptor_test.go +++ b/aes/decryptor_test.go @@ -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 } diff --git a/cmd/sops/main.go b/cmd/sops/main.go index ce5014fe8..2c0054822 100644 --- a/cmd/sops/main.go +++ b/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 +} diff --git a/sops.go b/sops.go index 8c279541c..b8f175414 100644 --- a/sops.go +++ b/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 } diff --git a/sops_test.go b/sops_test.go index 376612e13..9b3db3b27 100644 --- a/sops_test.go +++ b/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) }