Skip to content

Commit

Permalink
Add support for --unencrypted-regex (#715)
Browse files Browse the repository at this point in the history
* Add support for --unencrypted-regex

* Fix grammar mistake

* Add gofmt'd files
  • Loading branch information
renehernandez committed Sep 2, 2020
1 parent 4bd640e commit 8aca3cb
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 17 deletions.
13 changes: 12 additions & 1 deletion README.rst
Expand Up @@ -1426,9 +1426,20 @@ will encrypt the values under the ``data`` and ``stringData`` keys in a YAML fil
containing kubernetes secrets. It will not encrypt other values that help you to
navigate the file, like ``metadata`` which contains the secrets' names.
Conversely, you can opt in to only left certain keys without encrypting by using the
``--unencrypted-regex`` option, which will leave the values unencrypted of those keys
that match the supplied regular expression. For example, this command:
.. code:: bash
$ sops --encrypt --unencrypted-regex '^(description|metadata)$' k8s-secrets.yaml
will not encrypt the values under the ``description`` and ``metadata`` keys in a YAML file
containing kubernetes secrets, while encrypting everything else.
You can also specify these options in the ``.sops.yaml`` config file.
Note: these three options ``--unencrypted-suffix``, ``--encrypted-suffix``, and ``--encrypted-regex`` are
Note: these four options ``--unencrypted-suffix``, ``--encrypted-suffix``, ``--encrypted-regex`` and ``--unencrypted-regex`` are
mutually exclusive and cannot all be used in the same file.
Encryption Protocol
Expand Down
4 changes: 3 additions & 1 deletion cmd/sops/edit.go
Expand Up @@ -37,6 +37,7 @@ type editExampleOpts struct {
editOpts
UnencryptedSuffix string
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
KeyGroups []sops.KeyGroup
GroupThreshold int
Expand Down Expand Up @@ -66,6 +67,7 @@ func editExample(opts editExampleOpts) ([]byte, error) {
KeyGroups: opts.KeyGroups,
UnencryptedSuffix: opts.UnencryptedSuffix,
EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedRegex: opts.UnencryptedRegex,
EncryptedRegex: opts.EncryptedRegex,
Version: version.Version,
ShamirThreshold: opts.GroupThreshold,
Expand Down Expand Up @@ -132,7 +134,7 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) {
if err != nil {
return nil, common.NewExitError(fmt.Sprintf("Could not write output file: %s", err), codes.CouldNotWriteOutputFile)
}

// Close temporary file, since Windows won't delete the file unless it's closed beforehand
defer tmpfile.Close()

Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/encrypt.go
Expand Up @@ -22,6 +22,7 @@ type encryptOpts struct {
KeyServices []keyservice.KeyServiceClient
UnencryptedSuffix string
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
KeyGroups []sops.KeyGroup
GroupThreshold int
Expand Down Expand Up @@ -77,6 +78,7 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) {
KeyGroups: opts.KeyGroups,
UnencryptedSuffix: opts.UnencryptedSuffix,
EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedRegex: opts.UnencryptedRegex,
EncryptedRegex: opts.EncryptedRegex,
Version: version.Version,
ShamirThreshold: opts.GroupThreshold,
Expand Down
21 changes: 17 additions & 4 deletions cmd/sops/main.go
Expand Up @@ -185,9 +185,9 @@ func main() {
Usage: "the user to run the command as",
},
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
},
Name: "input-type",
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Expand Down Expand Up @@ -625,6 +625,10 @@ func main() {
Name: "encrypted-suffix",
Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
},
cli.StringFlag{
Name: "unencrypted-regex",
Usage: "set the unencrypted key suffix. 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.",
Expand Down Expand Up @@ -682,6 +686,7 @@ func main() {
unencryptedSuffix := c.String("unencrypted-suffix")
encryptedSuffix := c.String("encrypted-suffix")
encryptedRegex := c.String("encrypted-regex")
unencryptedRegex := c.String("unencrypted-regex")
conf, err := loadConfig(c, fileName, nil)
if err != nil {
return toExitError(err)
Expand All @@ -697,6 +702,9 @@ func main() {
if encryptedRegex == "" {
encryptedRegex = conf.EncryptedRegex
}
if unencryptedRegex == "" {
unencryptedRegex = conf.UnencryptedRegex
}
}

cryptRuleCount := 0
Expand All @@ -709,9 +717,12 @@ func main() {
if encryptedRegex != "" {
cryptRuleCount++
}
if unencryptedRegex != "" {
cryptRuleCount++
}

if cryptRuleCount > 1 {
return common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex in the same file", codes.ErrorConflictingParameters)
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)
}

// only supply the default UnencryptedSuffix when EncryptedSuffix and EncryptedRegex are not provided
Expand Down Expand Up @@ -742,6 +753,7 @@ func main() {
Cipher: aes.NewCipher(),
UnencryptedSuffix: unencryptedSuffix,
EncryptedSuffix: encryptedSuffix,
UnencryptedRegex: unencryptedRegex,
EncryptedRegex: encryptedRegex,
KeyServices: svcs,
KeyGroups: groups,
Expand Down Expand Up @@ -879,6 +891,7 @@ func main() {
editOpts: opts,
UnencryptedSuffix: unencryptedSuffix,
EncryptedSuffix: encryptedSuffix,
UnencryptedRegex: unencryptedRegex,
EncryptedRegex: encryptedRegex,
KeyGroups: groups,
GroupThreshold: threshold,
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Expand Up @@ -117,6 +117,7 @@ type creationRule struct {
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"`
}

Expand All @@ -135,6 +136,7 @@ type Config struct {
ShamirThreshold int
UnencryptedSuffix string
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
Destination publish.Destination
OmitExtensions bool
Expand Down Expand Up @@ -235,6 +237,7 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
ShamirThreshold: rule.ShamirThreshold,
UnencryptedSuffix: rule.UnencryptedSuffix,
EncryptedSuffix: rule.EncryptedSuffix,
UnencryptedRegex: rule.UnencryptedRegex,
EncryptedRegex: rule.EncryptedRegex,
}, nil
}
Expand Down
7 changes: 7 additions & 0 deletions config/config_test.go
Expand Up @@ -137,6 +137,7 @@ creation_rules:
kms: "1"
pgp: "2"
encrypted_regex: "^enc:"
unencrypted_regex: "^dec:"
`)

var sampleConfigWithInvalidParameters = []byte(`
Expand Down Expand Up @@ -349,6 +350,12 @@ func TestLoadConfigFileWithEncryptedSuffix(t *testing.T) {
assert.Equal(t, "_enc", conf.EncryptedSuffix)
}

func TestLoadConfigFileWithUnencryptedRegex(t *testing.T) {
conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "barbar", nil)
assert.Equal(t, nil, err)
assert.Equal(t, "^dec:", conf.UnencryptedRegex)
}

func TestLoadConfigFileWithEncryptedRegex(t *testing.T) {
conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithRegexParameters, t), "barbar", nil)
assert.Equal(t, nil, err)
Expand Down
4 changes: 2 additions & 2 deletions hcvault/keysource.go
Expand Up @@ -29,7 +29,7 @@ func init() {
type MasterKey struct {
EncryptedKey string
KeyName string
EnginePath string
EnginePath string
VaultAddress string
CreationDate time.Time
}
Expand Down Expand Up @@ -100,7 +100,7 @@ func getBackendAndKeyFromPath(fullPath string) (enginePath, keyName string, err
func NewMasterKey(addess, enginePath, keyName string) *MasterKey {
mk := &MasterKey{
VaultAddress: addess,
EnginePath: enginePath,
EnginePath: enginePath,
KeyName: keyName,
CreationDate: time.Now().UTC(),
}
Expand Down
4 changes: 2 additions & 2 deletions keyservice/keyservice.go
Expand Up @@ -39,8 +39,8 @@ func KeyFromMasterKey(mk keys.MasterKey) Key {
KeyType: &Key_VaultKey{
VaultKey: &VaultKey{
VaultAddress: mk.VaultAddress,
EnginePath: mk.EnginePath,
KeyName: mk.KeyName,
EnginePath: mk.EnginePath,
KeyName: mk.KeyName,
},
},
}
Expand Down
2 changes: 1 addition & 1 deletion keyservice/keyservice.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 25 additions & 4 deletions sops.go
Expand Up @@ -287,9 +287,10 @@ func (branch TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(
// 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), or those not matching EncryptedRegex,
// if EncryptedRegex is provided (by default it is not). If encryption is
// successful, it returns the MAC for the encrypted tree.
// 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 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{
File: tree.FilePath,
Expand Down Expand Up @@ -323,6 +324,15 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (string, error) {
}
}
}
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 {
Expand Down Expand Up @@ -358,7 +368,8 @@ func (tree Tree) Encrypt(key []byte, cipher Cipher) (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,
// those not ending with EncryptedSuffix, if EncryptedSuffix is provided (by default it is not),
// or those not matching EncryptedRegex, if EncryptedRegex 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 decryption is successful, it returns the MAC for the decrypted tree.
func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
log.Debug("Decrypting tree")
Expand Down Expand Up @@ -386,6 +397,15 @@ func (tree Tree) Decrypt(key []byte, cipher Cipher) (string, error) {
}
}
}
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 {
Expand Down Expand Up @@ -466,6 +486,7 @@ type Metadata struct {
LastModified time.Time
UnencryptedSuffix string
EncryptedSuffix string
UnencryptedRegex string
EncryptedRegex string
MessageAuthenticationCode string
Version string
Expand Down
53 changes: 53 additions & 0 deletions sops_test.go
Expand Up @@ -189,6 +189,59 @@ func TestEncryptedRegex(t *testing.T) {
}
}

func TestUnencryptedRegex(t *testing.T) {
branches := TreeBranches{
TreeBranch{
TreeItem{
Key: "dec:foo",
Value: "bar",
},
TreeItem{
Key: "dec:bar",
Value: TreeBranch{
TreeItem{
Key: "foo",
Value: "bar",
},
},
},
},
}
tree := Tree{Branches: branches, Metadata: Metadata{UnencryptedRegex: "^dec:"}}
expected := TreeBranch{
TreeItem{
Key: "dec:foo",
Value: "bar",
},
TreeItem{
Key: "dec:bar",
Value: TreeBranch{
TreeItem{
Key: "foo",
Value: "bar",
},
},
},
}
cipher := reverseCipher{}

_, err := tree.Encrypt(bytes.Repeat([]byte("f"), 32), cipher)
if err != nil {
t.Errorf("Encrypting the tree failed: %s", err)
}
// expected[1].Value[] = "bar"
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)
}
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) {
Expand Down
2 changes: 1 addition & 1 deletion stores/dotenv/store_test.go
Expand Up @@ -62,4 +62,4 @@ func TestEmitValueString(t *testing.T) {
func TestEmitValueNonstring(t *testing.T) {
_, err := (&Store{}).EmitValue(BRANCH)
assert.NotNil(t, err)
}
}
8 changes: 7 additions & 1 deletion stores/stores.go
Expand Up @@ -47,6 +47,7 @@ type Metadata struct {
PGPKeys []pgpkey `yaml:"pgp" json:"pgp"`
UnencryptedSuffix string `yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty"`
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"`
Version string `yaml:"version" json:"version"`
}
Expand Down Expand Up @@ -102,6 +103,7 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata {
m.LastModified = sopsMetadata.LastModified.Format(time.RFC3339)
m.UnencryptedSuffix = sopsMetadata.UnencryptedSuffix
m.EncryptedSuffix = sopsMetadata.EncryptedSuffix
m.UnencryptedRegex = sopsMetadata.UnencryptedRegex
m.EncryptedRegex = sopsMetadata.EncryptedRegex
m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode
m.Version = sopsMetadata.Version
Expand Down Expand Up @@ -222,12 +224,15 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
if m.EncryptedSuffix != "" {
cryptRuleCount++
}
if m.UnencryptedRegex != "" {
cryptRuleCount++
}
if m.EncryptedRegex != "" {
cryptRuleCount++
}

if cryptRuleCount > 1 {
return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex in the same file")
return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex or unencrypted_regex in the same file")
}

if cryptRuleCount == 0 {
Expand All @@ -240,6 +245,7 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
MessageAuthenticationCode: m.MessageAuthenticationCode,
UnencryptedSuffix: m.UnencryptedSuffix,
EncryptedSuffix: m.EncryptedSuffix,
UnencryptedRegex: m.UnencryptedRegex,
EncryptedRegex: m.EncryptedRegex,
LastModified: lastModified,
}, nil
Expand Down

0 comments on commit 8aca3cb

Please sign in to comment.