diff --git a/README.rst b/README.rst index 042999af3..3ac135087 100644 --- a/README.rst +++ b/README.rst @@ -1565,9 +1565,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/encrypt.go b/cmd/sops/encrypt.go index cd6008f17..ace7d8c2c 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -15,13 +15,15 @@ import ( ) type encryptConfig struct { - UnencryptedSuffix string - EncryptedSuffix string - UnencryptedRegex string - EncryptedRegex string - MACOnlyEncrypted bool - KeyGroups []sops.KeyGroup - GroupThreshold int + UnencryptedSuffix string + EncryptedSuffix string + UnencryptedRegex string + EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string + MACOnlyEncrypted bool + KeyGroups []sops.KeyGroup + GroupThreshold int } type encryptOpts struct { @@ -61,14 +63,16 @@ func ensureNoMetadata(opts encryptOpts, branch sops.TreeBranch) error { func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata { return sops.Metadata{ - KeyGroups: config.KeyGroups, - UnencryptedSuffix: config.UnencryptedSuffix, - EncryptedSuffix: config.EncryptedSuffix, - UnencryptedRegex: config.UnencryptedRegex, - EncryptedRegex: config.EncryptedRegex, - MACOnlyEncrypted: config.MACOnlyEncrypted, - Version: version.Version, - ShamirThreshold: config.GroupThreshold, + KeyGroups: config.KeyGroups, + UnencryptedSuffix: config.UnencryptedSuffix, + EncryptedSuffix: config.EncryptedSuffix, + UnencryptedRegex: config.UnencryptedRegex, + EncryptedRegex: config.EncryptedRegex, + UnencryptedCommentRegex: config.UnencryptedCommentRegex, + EncryptedCommentRegex: config.EncryptedCommentRegex, + MACOnlyEncrypted: config.MACOnlyEncrypted, + Version: version.Version, + ShamirThreshold: config.GroupThreshold, } } diff --git a/cmd/sops/main.go b/cmd/sops/main.go index bc888428d..155eaec78 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -1581,6 +1581,14 @@ func main() { Name: "encrypted-regex", 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", Usage: "path to sops' config file. If set, sops will not search for the config file recursively.", @@ -1839,6 +1847,8 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) { 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") macOnlyEncrypted := c.Bool("mac-only-encrypted") conf, err := loadConfig(c, fileName, nil) if err != nil { @@ -1858,6 +1868,12 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) { if unencryptedRegex == "" { unencryptedRegex = conf.UnencryptedRegex } + if encryptedCommentRegex == "" { + encryptedCommentRegex = conf.EncryptedCommentRegex + } + if unencryptedCommentRegex == "" { + unencryptedCommentRegex = conf.UnencryptedCommentRegex + } if !macOnlyEncrypted { macOnlyEncrypted = conf.MACOnlyEncrypted } @@ -1876,9 +1892,15 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) { if unencryptedRegex != "" { cryptRuleCount++ } + if encryptedCommentRegex != "" { + cryptRuleCount++ + } + if unencryptedCommentRegex != "" { + cryptRuleCount++ + } if cryptRuleCount > 1 { - return encryptConfig{}, 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 encryptConfig{}, 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, EncryptedRegex, and others are not provided @@ -1899,13 +1921,15 @@ func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) { } return encryptConfig{ - UnencryptedSuffix: unencryptedSuffix, - EncryptedSuffix: encryptedSuffix, - UnencryptedRegex: unencryptedRegex, - EncryptedRegex: encryptedRegex, - MACOnlyEncrypted: macOnlyEncrypted, - KeyGroups: groups, - GroupThreshold: threshold, + UnencryptedSuffix: unencryptedSuffix, + EncryptedSuffix: encryptedSuffix, + UnencryptedRegex: unencryptedRegex, + EncryptedRegex: encryptedRegex, + UnencryptedCommentRegex: unencryptedCommentRegex, + EncryptedCommentRegex: encryptedCommentRegex, + MACOnlyEncrypted: macOnlyEncrypted, + KeyGroups: groups, + GroupThreshold: threshold, }, nil } diff --git a/config/config.go b/config/config.go index 643268682..7565091fd 100644 --- a/config/config.go +++ b/config/config.go @@ -134,21 +134,23 @@ 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"` - MACOnlyEncrypted bool `yaml:"mac_only_encrypted"` + 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"` + MACOnlyEncrypted bool `yaml:"mac_only_encrypted"` } func NewStoresConfig() *StoresConfig { @@ -169,15 +171,17 @@ 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 - MACOnlyEncrypted bool - Destination publish.Destination - OmitExtensions bool + KeyGroups []sops.KeyGroup + ShamirThreshold int + UnencryptedSuffix string + EncryptedSuffix string + UnencryptedRegex string + EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string + MACOnlyEncrypted bool + Destination publish.Destination + OmitExtensions bool } func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) { @@ -283,9 +287,15 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) 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, encrypted_regex, or unencrypted_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) @@ -294,13 +304,15 @@ 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, - MACOnlyEncrypted: rule.MACOnlyEncrypted, + KeyGroups: groups, + ShamirThreshold: rule.ShamirThreshold, + UnencryptedSuffix: rule.UnencryptedSuffix, + EncryptedSuffix: rule.EncryptedSuffix, + UnencryptedRegex: rule.UnencryptedRegex, + EncryptedRegex: rule.EncryptedRegex, + UnencryptedCommentRegex: rule.UnencryptedCommentRegex, + EncryptedCommentRegex: rule.EncryptedCommentRegex, + MACOnlyEncrypted: rule.MACOnlyEncrypted, }, nil } diff --git a/config/config_test.go b/config/config_test.go index 8f4fb006b..857b50cb8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -166,6 +166,22 @@ creation_rules: mac_only_encrypted: true `) +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* @@ -430,6 +446,18 @@ func TestLoadConfigFileWithMACOnlyEncrypted(t *testing.T) { assert.Equal(t, true, conf.MACOnlyEncrypted) } +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 2063607a0..798291f3a 100644 --- a/sops.go +++ b/sops.go @@ -285,24 +285,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 @@ -312,21 +312,38 @@ 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) { + // Because append returns a new slice, the original stack is not changed. + commentsStack = append(commentsStack, []string{}) for i, v := range in { - newV, err := branch.walkValue(v, path, onLeaves) + c, vIsComment := v.(Comment) + if vIsComment { + // If v is a comment, we add it to the slice of active comments. + // This allows us to also encrypt comments themselves by enabling encryption in a prior comment. + 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 !vIsComment { + // If v is not a comment, we clear the slice of active comments. + 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) { + // Because append returns a new slice, the original stack is not changed. + 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 { + // If key is a comment, we add it to the slice of active comments. + // This allows us to also encrypt comments themselves by enabling encryption in a prior comment. + 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 } @@ -340,26 +357,113 @@ 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) } } + c, valueIsComment := item.Value.(Comment) + if valueIsComment { + // If value is a comment, we add it to the slice of active comments. + // This allows us to also encrypt comments themselves by enabling encryption in a prior comment. + 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 !valueIsComment { + // If value is not a comment, we clear the slice of active comments. + 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 // (all values if MACOnlyEncrypted is false, or only over values which end // up encrypted if MACOnlyEncrypted is true). @@ -374,47 +478,12 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) { hash.Write(MACOnlyEncryptedInitialization) } 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 _, 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 - } - } - } + _, err := branch.walkBranch(branch, make([]string, 0), make([][]string, 0), func(in interface{}, path []string, commentsStack [][]string) (interface{}, error) { + _, ok := in.(Comment) + encrypted := tree.shouldBeEncrypted(path, commentsStack, ok) if !tree.Metadata.MACOnlyEncrypted || encrypted { // 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) @@ -464,49 +533,14 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) { hash.Write(MACOnlyEncryptedInitialization) } 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 @@ -576,6 +610,8 @@ type Metadata struct { EncryptedSuffix string UnencryptedRegex string EncryptedRegex string + UnencryptedCommentRegex string + EncryptedCommentRegex string MessageAuthenticationCode string MACOnlyEncrypted bool Version string diff --git a/sops_test.go b/sops_test.go index 81ad23c99..e7f8623f6 100644 --- a/sops_test.go +++ b/sops_test.go @@ -330,6 +330,275 @@ func TestMACOnlyEncryptedNoConfusion(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 9ab308d14..169e8dbb5 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -56,6 +56,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"` MACOnlyEncrypted bool `yaml:"mac_only_encrypted,omitempty" json:"mac_only_encrypted,omitempty"` Version string `yaml:"version" json:"version"` } @@ -119,6 +121,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.MACOnlyEncrypted = sopsMetadata.MACOnlyEncrypted m.Version = sopsMetadata.Version @@ -260,9 +264,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 { @@ -277,6 +287,8 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) { EncryptedSuffix: m.EncryptedSuffix, UnencryptedRegex: m.UnencryptedRegex, EncryptedRegex: m.EncryptedRegex, + UnencryptedCommentRegex: m.UnencryptedCommentRegex, + EncryptedCommentRegex: m.EncryptedCommentRegex, MACOnlyEncrypted: m.MACOnlyEncrypted, LastModified: lastModified, }, nil