diff --git a/README.rst b/README.rst index 45abff5397..27e4002ff3 100644 --- a/README.rst +++ b/README.rst @@ -1438,9 +1438,18 @@ that match the supplied regular expression. For example, this command: will not encrypt the values under the ``description`` and ``metadata`` keys in a YAML file containing kubernetes secrets, while encrypting everything else. +For YAML files, another method is to use ``--encrypted-comment-regex`` which will +only encrypt comments and values which have a preceding comment matching the supplied +regular expression. + +Conversely, you can opt in to only left certain keys without encrypting by using the +``--unencrypted-comment-regex`` option, which will leave the values and comments +unencrypted when they have a preeceding comment that matches the supplied regular expression. + You can also specify these options in the ``.sops.yaml`` config file. -Note: these four options ``--unencrypted-suffix``, ``--encrypted-suffix``, ``--encrypted-regex`` and ``--unencrypted-regex`` are +Note: these six options ``--unencrypted-suffix``, ``--encrypted-suffix``, ``--encrypted-regex``, +``--unencrypted-regex``, ``--encrypted-comment-regex``, and ``--unencrypted-comment-regex`` are mutually exclusive and cannot all be used in the same file. Encryption Protocol diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 4ac92e487d..bda80d25ff 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -31,12 +31,14 @@ type editOpts struct { type editExampleOpts struct { editOpts - UnencryptedSuffix string - EncryptedSuffix string - UnencryptedRegex string - EncryptedRegex string - KeyGroups []sops.KeyGroup - GroupThreshold int + UnencryptedSuffix string + EncryptedSuffix string + UnencryptedRegex string + EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string + KeyGroups []sops.KeyGroup + GroupThreshold int } type runEditorUntilOkOpts struct { @@ -60,13 +62,15 @@ func editExample(opts editExampleOpts) ([]byte, error) { tree := sops.Tree{ Branches: branches, Metadata: sops.Metadata{ - KeyGroups: opts.KeyGroups, - UnencryptedSuffix: opts.UnencryptedSuffix, - EncryptedSuffix: opts.EncryptedSuffix, - UnencryptedRegex: opts.UnencryptedRegex, - EncryptedRegex: opts.EncryptedRegex, - Version: version.Version, - ShamirThreshold: opts.GroupThreshold, + KeyGroups: opts.KeyGroups, + UnencryptedSuffix: opts.UnencryptedSuffix, + EncryptedSuffix: opts.EncryptedSuffix, + UnencryptedRegex: opts.UnencryptedRegex, + EncryptedRegex: opts.EncryptedRegex, + UnencryptedCommentRegex: opts.UnencryptedCommentRegex, + EncryptedCommentRegex: opts.EncryptedCommentRegex, + Version: version.Version, + ShamirThreshold: opts.GroupThreshold, }, FilePath: path, } diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go index f5b770e7a6..f723c65fe1 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -14,17 +14,19 @@ import ( ) type encryptOpts struct { - Cipher sops.Cipher - InputStore sops.Store - OutputStore sops.Store - InputPath string - KeyServices []keyservice.KeyServiceClient - UnencryptedSuffix string - EncryptedSuffix string - UnencryptedRegex string - EncryptedRegex string - KeyGroups []sops.KeyGroup - GroupThreshold int + Cipher sops.Cipher + InputStore sops.Store + OutputStore sops.Store + InputPath string + KeyServices []keyservice.KeyServiceClient + UnencryptedSuffix string + EncryptedSuffix string + UnencryptedRegex string + EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string + KeyGroups []sops.KeyGroup + GroupThreshold int } type fileAlreadyEncryptedError struct{} @@ -77,13 +79,15 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { tree := sops.Tree{ Branches: branches, Metadata: sops.Metadata{ - KeyGroups: opts.KeyGroups, - UnencryptedSuffix: opts.UnencryptedSuffix, - EncryptedSuffix: opts.EncryptedSuffix, - UnencryptedRegex: opts.UnencryptedRegex, - EncryptedRegex: opts.EncryptedRegex, - Version: version.Version, - ShamirThreshold: opts.GroupThreshold, + KeyGroups: opts.KeyGroups, + UnencryptedSuffix: opts.UnencryptedSuffix, + EncryptedSuffix: opts.EncryptedSuffix, + UnencryptedRegex: opts.UnencryptedRegex, + EncryptedRegex: opts.EncryptedRegex, + UnencryptedCommentRegex: opts.UnencryptedCommentRegex, + EncryptedCommentRegex: opts.EncryptedCommentRegex, + Version: version.Version, + ShamirThreshold: opts.GroupThreshold, }, FilePath: path, } diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 50ec203553..01ef19a35a 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -678,11 +678,19 @@ func main() { }, cli.StringFlag{ Name: "unencrypted-regex", - Usage: "set the unencrypted key suffix. When specified, only keys matching the regex will be left unencrypted.", + Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.", }, cli.StringFlag{ Name: "encrypted-regex", - Usage: "set the encrypted key suffix. When specified, only keys matching the regex will be encrypted.", + Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.", + }, + cli.StringFlag{ + Name: "unencrypted-comment-regex", + Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.", + }, + cli.StringFlag{ + Name: "encrypted-comment-regex", + Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.", }, cli.StringFlag{ Name: "config", @@ -738,6 +746,8 @@ func main() { encryptedSuffix := c.String("encrypted-suffix") encryptedRegex := c.String("encrypted-regex") unencryptedRegex := c.String("unencrypted-regex") + encryptedCommentRegex := c.String("encrypted-comment-regex") + unencryptedCommentRegex := c.String("unencrypted-comment-regex") conf, err := loadConfig(c, fileName, nil) if err != nil { return toExitError(err) @@ -756,6 +766,12 @@ func main() { if unencryptedRegex == "" { unencryptedRegex = conf.UnencryptedRegex } + if encryptedCommentRegex == "" { + encryptedCommentRegex = conf.EncryptedCommentRegex + } + if unencryptedCommentRegex == "" { + unencryptedCommentRegex = conf.UnencryptedCommentRegex + } } cryptRuleCount := 0 @@ -771,12 +787,18 @@ func main() { if unencryptedRegex != "" { cryptRuleCount++ } + if encryptedCommentRegex != "" { + cryptRuleCount++ + } + if unencryptedCommentRegex != "" { + cryptRuleCount++ + } if cryptRuleCount > 1 { - return common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex or unencrypted_regex in the same file", codes.ErrorConflictingParameters) + return common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file", codes.ErrorConflictingParameters) } - // only supply the default UnencryptedSuffix when EncryptedSuffix and EncryptedRegex are not provided + // only supply the default UnencryptedSuffix when EncryptedSuffix, EncryptedRegex, and others are not provided if cryptRuleCount == 0 { unencryptedSuffix = sops.DefaultUnencryptedSuffix } @@ -798,17 +820,19 @@ func main() { return toExitError(err) } output, err = encrypt(encryptOpts{ - OutputStore: outputStore, - InputStore: inputStore, - InputPath: fileName, - Cipher: aes.NewCipher(), - UnencryptedSuffix: unencryptedSuffix, - EncryptedSuffix: encryptedSuffix, - UnencryptedRegex: unencryptedRegex, - EncryptedRegex: encryptedRegex, - KeyServices: svcs, - KeyGroups: groups, - GroupThreshold: threshold, + OutputStore: outputStore, + InputStore: inputStore, + InputPath: fileName, + Cipher: aes.NewCipher(), + UnencryptedSuffix: unencryptedSuffix, + EncryptedSuffix: encryptedSuffix, + UnencryptedRegex: unencryptedRegex, + EncryptedRegex: encryptedRegex, + UnencryptedCommentRegex: unencryptedCommentRegex, + EncryptedCommentRegex: encryptedCommentRegex, + KeyServices: svcs, + KeyGroups: groups, + GroupThreshold: threshold, }) } @@ -953,13 +977,15 @@ func main() { return toExitError(err) } output, err = editExample(editExampleOpts{ - editOpts: opts, - UnencryptedSuffix: unencryptedSuffix, - EncryptedSuffix: encryptedSuffix, - UnencryptedRegex: unencryptedRegex, - EncryptedRegex: encryptedRegex, - KeyGroups: groups, - GroupThreshold: threshold, + editOpts: opts, + UnencryptedSuffix: unencryptedSuffix, + EncryptedSuffix: encryptedSuffix, + UnencryptedRegex: unencryptedRegex, + EncryptedRegex: encryptedRegex, + UnencryptedCommentRegex: unencryptedCommentRegex, + EncryptedCommentRegex: encryptedCommentRegex, + KeyGroups: groups, + GroupThreshold: threshold, }) } } diff --git a/config/config.go b/config/config.go index 3116046344..f48b84a76f 100644 --- a/config/config.go +++ b/config/config.go @@ -109,20 +109,22 @@ type destinationRule struct { } type creationRule struct { - PathRegex string `yaml:"path_regex"` - KMS string - AwsProfile string `yaml:"aws_profile"` - Age string `yaml:"age"` - PGP string - GCPKMS string `yaml:"gcp_kms"` - AzureKeyVault string `yaml:"azure_keyvault"` - VaultURI string `yaml:"hc_vault_transit_uri"` - KeyGroups []keyGroup `yaml:"key_groups"` - ShamirThreshold int `yaml:"shamir_threshold"` - UnencryptedSuffix string `yaml:"unencrypted_suffix"` - EncryptedSuffix string `yaml:"encrypted_suffix"` - UnencryptedRegex string `yaml:"unencrypted_regex"` - EncryptedRegex string `yaml:"encrypted_regex"` + PathRegex string `yaml:"path_regex"` + KMS string + AwsProfile string `yaml:"aws_profile"` + Age string `yaml:"age"` + PGP string + GCPKMS string `yaml:"gcp_kms"` + AzureKeyVault string `yaml:"azure_keyvault"` + VaultURI string `yaml:"hc_vault_transit_uri"` + KeyGroups []keyGroup `yaml:"key_groups"` + ShamirThreshold int `yaml:"shamir_threshold"` + UnencryptedSuffix string `yaml:"unencrypted_suffix"` + EncryptedSuffix string `yaml:"encrypted_suffix"` + UnencryptedRegex string `yaml:"unencrypted_regex"` + EncryptedRegex string `yaml:"encrypted_regex"` + UnencryptedCommentRegex string `yaml:"unencrypted_comment_regex"` + EncryptedCommentRegex string `yaml:"encrypted_comment_regex"` } // Load loads a sops config file into a temporary struct @@ -136,14 +138,16 @@ func (f *configFile) load(bytes []byte) error { // Config is the configuration for a given SOPS file type Config struct { - KeyGroups []sops.KeyGroup - ShamirThreshold int - UnencryptedSuffix string - EncryptedSuffix string - UnencryptedRegex string - EncryptedRegex string - Destination publish.Destination - OmitExtensions bool + KeyGroups []sops.KeyGroup + ShamirThreshold int + UnencryptedSuffix string + EncryptedSuffix string + UnencryptedRegex string + EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string + Destination publish.Destination + OmitExtensions bool } func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) { @@ -242,12 +246,21 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) if rule.EncryptedSuffix != "" { cryptRuleCount++ } + if rule.UnencryptedRegex != "" { + cryptRuleCount++ + } if rule.EncryptedRegex != "" { cryptRuleCount++ } + if rule.UnencryptedCommentRegex != "" { + cryptRuleCount++ + } + if rule.EncryptedCommentRegex != "" { + cryptRuleCount++ + } if cryptRuleCount > 1 { - return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex for the same rule") + return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex for the same rule") } groups, err := getKeyGroupsFromCreationRule(rule, kmsEncryptionContext) @@ -256,12 +269,14 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) } return &Config{ - KeyGroups: groups, - ShamirThreshold: rule.ShamirThreshold, - UnencryptedSuffix: rule.UnencryptedSuffix, - EncryptedSuffix: rule.EncryptedSuffix, - UnencryptedRegex: rule.UnencryptedRegex, - EncryptedRegex: rule.EncryptedRegex, + KeyGroups: groups, + ShamirThreshold: rule.ShamirThreshold, + UnencryptedSuffix: rule.UnencryptedSuffix, + EncryptedSuffix: rule.EncryptedSuffix, + UnencryptedRegex: rule.UnencryptedRegex, + EncryptedRegex: rule.EncryptedRegex, + UnencryptedCommentRegex: rule.UnencryptedCommentRegex, + EncryptedCommentRegex: rule.EncryptedCommentRegex, }, nil } diff --git a/config/config_test.go b/config/config_test.go index a653fcb8ed..e6f1f74f15 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -140,15 +140,38 @@ creation_rules: version: fooversion `) -var sampleConfigWithRegexParameters = []byte(` +var sampleConfigWithEncryptedRegexParameters = []byte(` creation_rules: - path_regex: barbar* kms: "1" pgp: "2" encrypted_regex: "^enc:" + `) + +var sampleConfigWithUnencryptedRegexParameters = []byte(` +creation_rules: + - path_regex: barbar* + kms: "1" + pgp: "2" unencrypted_regex: "^dec:" `) +var sampleConfigWithEncryptedCommentRegexParameters = []byte(` +creation_rules: + - path_regex: barbar* + kms: "1" + pgp: "2" + encrypted_comment_regex: "sops:enc" + `) + +var sampleConfigWithUnencryptedCommentRegexParameters = []byte(` +creation_rules: + - path_regex: barbar* + kms: "1" + pgp: "2" + unencrypted_comment_regex: "sops:dec" + `) + var sampleConfigWithInvalidParameters = []byte(` creation_rules: - path_regex: foobar* @@ -396,17 +419,29 @@ func TestLoadConfigFileWithEncryptedSuffix(t *testing.T) { } func TestLoadConfigFileWithUnencryptedRegex(t *testing.T) { - conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "/conf/path", "barbar", nil) + conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithUnencryptedRegexParameters, t), "/conf/path", "barbar", nil) assert.Equal(t, nil, err) assert.Equal(t, "^dec:", conf.UnencryptedRegex) } func TestLoadConfigFileWithEncryptedRegex(t *testing.T) { - conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "/conf/path", "barbar", nil) + conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithEncryptedRegexParameters, t), "/conf/path", "barbar", nil) assert.Equal(t, nil, err) assert.Equal(t, "^enc:", conf.EncryptedRegex) } +func TestLoadConfigFileWithUnencryptedCommentRegex(t *testing.T) { + conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithUnencryptedCommentRegexParameters, t), "/conf/path", "barbar", nil) + assert.Equal(t, nil, err) + assert.Equal(t, "sops:dec", conf.UnencryptedCommentRegex) +} + +func TestLoadConfigFileWithEncryptedCommentRegex(t *testing.T) { + conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithEncryptedCommentRegexParameters, t), "/conf/path", "barbar", nil) + assert.Equal(t, nil, err) + assert.Equal(t, "sops:enc", conf.EncryptedCommentRegex) +} + func TestLoadConfigFileWithInvalidParameters(t *testing.T) { _, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil) assert.NotNil(t, err) diff --git a/sops.go b/sops.go index ae0ecfc244..c822d4118e 100644 --- a/sops.go +++ b/sops.go @@ -216,24 +216,24 @@ func (branch TreeBranch) Truncate(path []interface{}) (interface{}, error) { return current, nil } -func (branch TreeBranch) walkValue(in interface{}, path []string, onLeaves func(in interface{}, path []string) (interface{}, error)) (interface{}, error) { +func (branch TreeBranch) walkValue(in interface{}, path []string, commentsStack [][]string, onLeaves func(in interface{}, path []string, commentsStack [][]string) (interface{}, error)) (interface{}, error) { switch in := in.(type) { case string: - return onLeaves(in, path) + return onLeaves(in, path, commentsStack) case []byte: - return onLeaves(string(in), path) + return onLeaves(string(in), path, commentsStack) case int: - return onLeaves(in, path) + return onLeaves(in, path, commentsStack) case bool: - return onLeaves(in, path) + return onLeaves(in, path, commentsStack) case float64: - return onLeaves(in, path) + return onLeaves(in, path, commentsStack) case Comment: - return onLeaves(in, path) + return onLeaves(in, path, commentsStack) case TreeBranch: - return branch.walkBranch(in, path, onLeaves) + return branch.walkBranch(in, path, commentsStack, onLeaves) case []interface{}: - return branch.walkSlice(in, path, onLeaves) + return branch.walkSlice(in, path, commentsStack, onLeaves) case nil: // the value returned remains the same since it doesn't make // sense to encrypt or decrypt a nil value @@ -243,21 +243,30 @@ func (branch TreeBranch) walkValue(in interface{}, path []string, onLeaves func( } } -func (branch TreeBranch) walkSlice(in []interface{}, path []string, onLeaves func(in interface{}, path []string) (interface{}, error)) ([]interface{}, error) { +func (branch TreeBranch) walkSlice(in []interface{}, path []string, commentsStack [][]string, onLeaves func(in interface{}, path []string, commentsStack [][]string) (interface{}, error)) ([]interface{}, error) { + commentsStack = append(commentsStack, []string{}) for i, v := range in { - newV, err := branch.walkValue(v, path, onLeaves) + if c, ok := v.(Comment); ok { + commentsStack[len(commentsStack)-1] = append(commentsStack[len(commentsStack)-1], c.Value) + } + newV, err := branch.walkValue(v, path, commentsStack, onLeaves) if err != nil { return nil, err } in[i] = newV + if _, ok := v.(Comment); !ok { + commentsStack[len(commentsStack)-1] = []string{} + } } return in, nil } -func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(in interface{}, path []string) (interface{}, error)) (TreeBranch, error) { +func (branch TreeBranch) walkBranch(in TreeBranch, path []string, commentsStack [][]string, onLeaves func(in interface{}, path []string, commentsStack [][]string) (interface{}, error)) (TreeBranch, error) { + commentsStack = append(commentsStack, []string{}) for i, item := range in { - if _, ok := item.Key.(Comment); ok { - enc, err := branch.walkValue(item.Key, path, onLeaves) + if c, ok := item.Key.(Comment); ok { + commentsStack[len(commentsStack)-1] = append(commentsStack[len(commentsStack)-1], c.Value) + enc, err := branch.walkValue(item.Key, path, commentsStack, onLeaves) if err != nil { return nil, err } @@ -271,26 +280,109 @@ func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func( return nil, fmt.Errorf("walkValue of Comment should be either Comment or string, was %T", enc) } } + if c, ok := item.Value.(Comment); ok { + commentsStack[len(commentsStack)-1] = append(commentsStack[len(commentsStack)-1], c.Value) + } key, ok := item.Key.(string) if !ok { return nil, fmt.Errorf("Tree contains a non-string key (type %T): %s. Only string keys are"+ "supported", item.Key, item.Key) } - newV, err := branch.walkValue(item.Value, append(path, key), onLeaves) + newV, err := branch.walkValue(item.Value, append(path, key), commentsStack, onLeaves) if err != nil { return nil, err } in[i].Value = newV + if _, ok := item.Value.(Comment); !ok { + commentsStack[len(commentsStack)-1] = []string{} + } } return in, nil } +func (tree Tree) shouldBeEncrypted(path []string, commentsStack [][]string, isComment bool) bool { + encrypted := true + if tree.Metadata.UnencryptedSuffix != "" { + for _, v := range path { + if strings.HasSuffix(v, tree.Metadata.UnencryptedSuffix) { + encrypted = false + break + } + } + } + if tree.Metadata.EncryptedSuffix != "" { + encrypted = false + for _, v := range path { + if strings.HasSuffix(v, tree.Metadata.EncryptedSuffix) { + encrypted = true + break + } + } + } + if tree.Metadata.UnencryptedRegex != "" { + for _, p := range path { + matched, _ := regexp.Match(tree.Metadata.UnencryptedRegex, []byte(p)) + if matched { + encrypted = false + break + } + } + } + if tree.Metadata.EncryptedRegex != "" { + encrypted = false + for _, p := range path { + matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p)) + if matched { + encrypted = true + break + } + } + } + if tree.Metadata.UnencryptedCommentRegex != "" { + unencryptedComments: + for _, cs := range commentsStack { + for _, c := range cs { + matched, _ := regexp.Match(tree.Metadata.UnencryptedCommentRegex, []byte(c)) + if matched { + encrypted = false + break unencryptedComments + } + } + } + } + if tree.Metadata.EncryptedCommentRegex != "" { + lenCommentsStack := len(commentsStack) + lenLastCommentsStack := len(commentsStack[lenCommentsStack-1]) + encrypted = false + encryptedComments: + for i, cs := range commentsStack { + for j, c := range cs { + // A special case. We do not encrypt the comment line itself which matches the regex. + // So we skip the last line of the last set of comments. Only if the matches any previous + // line, we encrypt this comment. Otherwise we do not. + if isComment && i == lenCommentsStack-1 && j == lenLastCommentsStack-1 { + continue + } + matched, _ := regexp.Match(tree.Metadata.EncryptedCommentRegex, []byte(c)) + if matched { + encrypted = true + break encryptedComments + } + } + } + } + return encrypted +} + // 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, those not ending with EncryptedSuffix, if EncryptedSuffix // is provided (by default it is not), those not matching EncryptedRegex, -// if EncryptedRegex is provided (by default it is not) or those matching -// UnencryptedRegex, if UnencryptedRegex is provided (by default it is not). +// if EncryptedRegex is provided (by default it is not), those matching UnencryptedRegex, +// if UnencryptedRegex is provided (by default it is not), those with their comment +// not matching EncryptedCommentRegex, if EncryptedCommentRegex is provided (by default +// it is not), or those with their comment matching UnencryptedCommentRegex, if +// UnencryptedCommentRegex is provided (by default it is not). // If encryption is successful, it returns the MAC for the encrypted tree. func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) { audit.SubmitEvent(audit.EncryptEvent{ @@ -298,52 +390,17 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) { }) hash := sha512.New() walk := func(branch TreeBranch) error { - _, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) { + _, err := branch.walkBranch(branch, make([]string, 0), make([][]string, 0), func(in interface{}, path []string, commentsStack [][]string) (interface{}, error) { + _, ok := in.(Comment) // Only add to MAC if not a comment - if _, ok := in.(Comment); !ok { + if !ok { bytes, err := ToBytes(in) if err != nil { return nil, fmt.Errorf("Could not convert %s to bytes: %s", in, err) } hash.Write(bytes) } - encrypted := true - if tree.Metadata.UnencryptedSuffix != "" { - for _, v := range path { - if strings.HasSuffix(v, tree.Metadata.UnencryptedSuffix) { - encrypted = false - break - } - } - } - if tree.Metadata.EncryptedSuffix != "" { - encrypted = false - for _, v := range path { - if strings.HasSuffix(v, tree.Metadata.EncryptedSuffix) { - encrypted = true - break - } - } - } - if tree.Metadata.UnencryptedRegex != "" { - for _, p := range path { - matched, _ := regexp.Match(tree.Metadata.UnencryptedRegex, []byte(p)) - if matched { - encrypted = false - break - } - } - } - if tree.Metadata.EncryptedRegex != "" { - encrypted = false - for _, p := range path { - matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p)) - if matched { - encrypted = true - break - } - } - } + encrypted := tree.shouldBeEncrypted(path, commentsStack, ok) if encrypted { var err error pathString := strings.Join(path, ":") + ":" @@ -379,49 +436,14 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) { }) hash := sha512.New() walk := func(branch TreeBranch) error { - _, err := branch.walkBranch(branch, make([]string, 0), func(in interface{}, path []string) (interface{}, error) { - encrypted := true - if tree.Metadata.UnencryptedSuffix != "" { - for _, p := range path { - if strings.HasSuffix(p, tree.Metadata.UnencryptedSuffix) { - encrypted = false - break - } - } - } - if tree.Metadata.EncryptedSuffix != "" { - encrypted = false - for _, p := range path { - if strings.HasSuffix(p, tree.Metadata.EncryptedSuffix) { - encrypted = true - break - } - } - } - if tree.Metadata.UnencryptedRegex != "" { - for _, p := range path { - matched, _ := regexp.Match(tree.Metadata.UnencryptedRegex, []byte(p)) - if matched { - encrypted = false - break - } - } - } - if tree.Metadata.EncryptedRegex != "" { - encrypted = false - for _, p := range path { - matched, _ := regexp.Match(tree.Metadata.EncryptedRegex, []byte(p)) - if matched { - encrypted = true - break - } - } - } + _, err := branch.walkBranch(branch, make([]string, 0), make([][]string, 0), func(in interface{}, path []string, commentsStack [][]string) (interface{}, error) { + c, ok := in.(Comment) + encrypted := tree.shouldBeEncrypted(path, commentsStack, ok) var v interface{} if encrypted { var err error pathString := strings.Join(path, ":") + ":" - if c, ok := in.(Comment); ok { + if ok { v, err = cipher.Decrypt(c.Value, key, pathString) if err != nil { // Assume the comment was not encrypted in the first place @@ -489,6 +511,8 @@ type Metadata struct { EncryptedSuffix string UnencryptedRegex string EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string MessageAuthenticationCode string Version string KeyGroups []KeyGroup diff --git a/sops_test.go b/sops_test.go index 549de80bfe..20c49e2ef5 100644 --- a/sops_test.go +++ b/sops_test.go @@ -242,6 +242,275 @@ func TestUnencryptedRegex(t *testing.T) { } } +func TestEncryptedCommentRegex(t *testing.T) { + branches := TreeBranches{ + TreeBranch{ + TreeItem{ + Key: Comment{"sops:enc"}, + Value: nil, + }, + TreeItem{ + Key: "foo", + Value: "bar", + }, + TreeItem{ + Key: "bar", + Value: TreeBranch{ + TreeItem{ + Key: "foo", + Value: "bar", + }, + TreeItem{ + Key: Comment{"before"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"sops:enc"}, + Value: nil, + }, + TreeItem{ + Key: "encrypted", + Value: "bar", + }, + }, + }, + TreeItem{ + Key: "array", + Value: []interface{}{ + "bar", + Comment{"sops:enc"}, + "baz", + }, + }, + TreeItem{ + Key: Comment{"sops:enc"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"after"}, + Value: nil, + }, + TreeItem{ + Key: "encarray", + Value: []interface{}{ + "bar", + "baz", + }, + }, + }, + } + tree := Tree{Branches: branches, Metadata: Metadata{EncryptedCommentRegex: "sops:enc"}} + expected := TreeBranch{ + TreeItem{ + Key: Comment{"sops:enc"}, + Value: nil, + }, + TreeItem{ + Key: "foo", + Value: "rab", + }, + TreeItem{ + Key: "bar", + Value: TreeBranch{ + TreeItem{ + Key: "foo", + Value: "bar", + }, + TreeItem{ + Key: Comment{"before"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"sops:enc"}, + Value: nil, + }, + TreeItem{ + Key: "encrypted", + Value: "rab", + }, + }, + }, + TreeItem{ + Key: "array", + Value: []interface{}{ + "bar", + Comment{"sops:enc"}, + "zab", + }, + }, + TreeItem{ + Key: Comment{"sops:enc"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"retfa"}, + Value: nil, + }, + TreeItem{ + Key: "encarray", + Value: []interface{}{ + "rab", + "zab", + }, + }, + } + cipher := reverseCipher{} + _, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Encrypting the tree failed: %s", err) + } + if !reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees don't match: \ngot \t\t%+v,\n expected \t\t%+v", tree.Branches[0], expected) + } + _, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Decrypting the tree failed: %s", err) + } + expected[1].Value = "bar" + expected[2].Value.(TreeBranch)[3].Value = "bar" + expected[3].Value.([]interface{})[2] = "baz" + expected[5].Key = Comment{"after"} + expected[6].Value = []interface{}{ + "bar", + "baz", + } + if !reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected) + } +} + +func TestUnencryptedCommentRegex(t *testing.T) { + branches := TreeBranches{ + TreeBranch{ + TreeItem{ + Key: Comment{"sops:noenc"}, + Value: nil, + }, + TreeItem{ + Key: "foo", + Value: "bar", + }, + TreeItem{ + Key: "bar", + Value: TreeBranch{ + TreeItem{ + Key: "foo", + Value: "bar", + }, + TreeItem{ + Key: Comment{"before"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"sops:noenc"}, + Value: nil, + }, + TreeItem{ + Key: "notencrypted", + Value: "bar", + }, + }, + }, + TreeItem{ + Key: "array", + Value: []interface{}{ + "bar", + Comment{"sops:noenc"}, + "baz", + }, + }, + TreeItem{ + Key: Comment{"sops:noenc"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"after"}, + Value: nil, + }, + TreeItem{ + Key: "notencarray", + Value: []interface{}{ + "bar", + "baz", + }, + }, + }, + } + tree := Tree{Branches: branches, Metadata: Metadata{UnencryptedCommentRegex: "sops:noenc"}} + expected := TreeBranch{ + TreeItem{ + Key: Comment{"sops:noenc"}, + Value: nil, + }, + TreeItem{ + Key: "foo", + Value: "bar", + }, + TreeItem{ + Key: "bar", + Value: TreeBranch{ + TreeItem{ + Key: "foo", + Value: "rab", + }, + TreeItem{ + Key: Comment{"erofeb"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"sops:noenc"}, + Value: nil, + }, + TreeItem{ + Key: "notencrypted", + Value: "bar", + }, + }, + }, + TreeItem{ + Key: "array", + Value: []interface{}{ + "rab", + Comment{"sops:noenc"}, + "baz", + }, + }, + TreeItem{ + Key: Comment{"sops:noenc"}, + Value: nil, + }, + TreeItem{ + Key: Comment{"after"}, + Value: nil, + }, + TreeItem{ + Key: "notencarray", + Value: []interface{}{ + "bar", + "baz", + }, + }, + } + cipher := reverseCipher{} + _, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Encrypting the tree failed: %s", err) + } + if !reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees don't match: \ngot \t\t%+v,\n expected \t\t%+v", tree.Branches[0], expected) + } + _, err = tree.Decrypt(bytes.Repeat([]byte("f"), 32), cipher) + if err != nil { + t.Errorf("Decrypting the tree failed: %s", err) + } + expected[2].Value.(TreeBranch)[0].Value = "bar" + expected[2].Value.(TreeBranch)[1].Key = Comment{"before"} + expected[3].Value.([]interface{})[0] = "bar" + if !reflect.DeepEqual(tree.Branches[0], expected) { + t.Errorf("Trees don't match: \ngot\t\t\t%+v,\nexpected\t\t%+v", tree.Branches[0], expected) + } +} + type MockCipher struct{} func (m MockCipher) Encrypt(value interface{}, key []byte, path string) (string, error) { diff --git a/stores/stores.go b/stores/stores.go index 420c115c75..37f4441bd7 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -51,6 +51,8 @@ type Metadata struct { EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"` UnencryptedRegex string `yaml:"unencrypted_regex,omitempty" json:"unencrypted_regex,omitempty"` EncryptedRegex string `yaml:"encrypted_regex,omitempty" json:"encrypted_regex,omitempty"` + UnencryptedCommentRegex string `yaml:"unencrypted_comment_regex,omitempty" json:"unencrypted_comment_regex,omitempty"` + EncryptedCommentRegex string `yaml:"encrypted_comment_regex,omitempty" json:"encrypted_comment_regex,omitempty"` Version string `yaml:"version" json:"version"` } @@ -113,6 +115,8 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata { m.EncryptedSuffix = sopsMetadata.EncryptedSuffix m.UnencryptedRegex = sopsMetadata.UnencryptedRegex m.EncryptedRegex = sopsMetadata.EncryptedRegex + m.UnencryptedCommentRegex = sopsMetadata.UnencryptedCommentRegex + m.EncryptedCommentRegex = sopsMetadata.EncryptedCommentRegex m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode m.Version = sopsMetadata.Version m.ShamirThreshold = sopsMetadata.ShamirThreshold @@ -253,9 +257,15 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) { if m.EncryptedRegex != "" { cryptRuleCount++ } + if m.UnencryptedCommentRegex != "" { + cryptRuleCount++ + } + if m.EncryptedCommentRegex != "" { + cryptRuleCount++ + } if cryptRuleCount > 1 { - return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex or unencrypted_regex in the same file") + return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file") } if cryptRuleCount == 0 { @@ -270,6 +280,8 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) { EncryptedSuffix: m.EncryptedSuffix, UnencryptedRegex: m.UnencryptedRegex, EncryptedRegex: m.EncryptedRegex, + UnencryptedCommentRegex: m.UnencryptedCommentRegex, + EncryptedCommentRegex: m.EncryptedCommentRegex, LastModified: lastModified, }, nil }