From ffc1e265bb266263d85bdd3569ca8cc7b296eb81 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 30 Aug 2025 11:04:09 +0200 Subject: [PATCH] Ignore encryption selection options for binary store (and warn when they are used). Signed-off-by: Felix Fontein --- cmd/sops/main.go | 39 +++++++++++++++++++++++++++++++++------ sops.go | 8 ++++++++ stores/dotenv/store.go | 8 ++++++++ stores/ini/store.go | 8 ++++++++ stores/json/store.go | 17 +++++++++++++++++ stores/yaml/store.go | 8 ++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index b4d0da79d..038977d14 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -1023,7 +1023,7 @@ func main() { } svcs := keyservices(c) - encConfig, err := getEncryptConfig(c, fileNameOverride, nil) + encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, nil) if err != nil { return toExitError(err) } @@ -1369,7 +1369,7 @@ func main() { } } else { // File doesn't exist, edit the example file instead - encConfig, err := getEncryptConfig(c, fileName, nil) + encConfig, err := getEncryptConfig(c, fileName, inputStore, nil) if err != nil { return toExitError(err) } @@ -1908,7 +1908,7 @@ func main() { } var output []byte if isEncryptMode { - encConfig, err := getEncryptConfig(c, fileNameOverride, config) + encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config) if err != nil { return toExitError(err) } @@ -1996,7 +1996,7 @@ func main() { output, err = edit(opts) } else { // File doesn't exist, edit the example file instead - encConfig, err := getEncryptConfig(c, fileNameOverride, config) + encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config) if err != nil { return toExitError(err) } @@ -2050,7 +2050,7 @@ func main() { } } -func getEncryptConfig(c *cli.Context, fileName string, optionalConfig *config.Config) (encryptConfig, error) { +func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store, optionalConfig *config.Config) (encryptConfig, error) { unencryptedSuffix := c.String("unencrypted-suffix") encryptedSuffix := c.String("encrypted-suffix") encryptedRegex := c.String("encrypted-regex") @@ -2090,6 +2090,33 @@ func getEncryptConfig(c *cli.Context, fileName string, optionalConfig *config.Co } } + if inputStore.IsSingleValueStore() { + // Warn about settings that potentially disable encryption of the single key. + if unencryptedSuffix != "" { + log.Warn(fmt.Sprintf("Using an unencrypted suffix does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name())) + } + if encryptedSuffix != "" { + log.Warn(fmt.Sprintf("Using an encrypted suffix does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name())) + } + if encryptedRegex != "" { + log.Warn(fmt.Sprintf("Using an encrypted regex does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name())) + } + if unencryptedRegex != "" { + log.Warn(fmt.Sprintf("Using an unencrypted regex does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name())) + } + if encryptedCommentRegex != "" { + log.Warn(fmt.Sprintf("Using an encrypted comment regex does not make sense with the input store (the %s store never produces comments) and will be ignored.", inputStore.Name())) + } + // Do not warn about unencryptedCommentRegex and macOnlyEncrypted since they cannot have any effect. + unencryptedSuffix = "" + encryptedSuffix = "" + encryptedRegex = "" + unencryptedRegex = "" + encryptedCommentRegex = "" + unencryptedCommentRegex = "" + macOnlyEncrypted = false + } + cryptRuleCount := 0 if unencryptedSuffix != "" { cryptRuleCount++ @@ -2115,7 +2142,7 @@ func getEncryptConfig(c *cli.Context, fileName string, optionalConfig *config.Co } // only supply the default UnencryptedSuffix when EncryptedSuffix, EncryptedRegex, and others are not provided - if cryptRuleCount == 0 { + if cryptRuleCount == 0 && !inputStore.IsSingleValueStore() { unencryptedSuffix = sops.DefaultUnencryptedSuffix } diff --git a/sops.go b/sops.go index a32211f1a..2347ec6a0 100644 --- a/sops.go +++ b/sops.go @@ -726,6 +726,12 @@ type CheckEncrypted interface { HasSopsTopLevelKey(TreeBranch) bool } +// SingleValueStore is the interface for determining whether a store uses only +// one single key and no comments. This is basically identifying the binary store. +type SingleValueStore interface { + IsSingleValueStore() bool +} + // Store is used to interact with files, both encrypted and unencrypted. type Store interface { EncryptedFileLoader @@ -734,6 +740,8 @@ type Store interface { PlainFileEmitter ValueEmitter CheckEncrypted + SingleValueStore + Name() string } // MasterKeyCount returns the number of master keys available diff --git a/stores/dotenv/store.go b/stores/dotenv/store.go index c6ec8b505..d30f3d8b7 100644 --- a/stores/dotenv/store.go +++ b/stores/dotenv/store.go @@ -23,6 +23,14 @@ func NewStore(c *config.DotenvStoreConfig) *Store { return &Store{config: *c} } +func (store *Store) IsSingleValueStore() bool { + return false +} + +func (store *Store) Name() string { + return "dotenv" +} + // LoadEncryptedFile loads an encrypted file's bytes onto a sops.Tree runtime object func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { branches, err := store.LoadPlainFile(in) diff --git a/stores/ini/store.go b/stores/ini/store.go index d8e72990d..e98cd5543 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -21,6 +21,14 @@ func NewStore(c *config.INIStoreConfig) *Store { return &Store{config: c} } +func (store *Store) IsSingleValueStore() bool { + return false +} + +func (store *Store) Name() string { + return "ini" +} + func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) { iniFile := ini.Empty(ini.LoadOptions{AllowNonUniqueSections: true}) iniFile.DeleteSection(ini.DefaultSection) diff --git a/stores/json/store.go b/stores/json/store.go index 7b8bf3da5..3491d3e88 100644 --- a/stores/json/store.go +++ b/stores/json/store.go @@ -22,12 +22,29 @@ func NewStore(c *config.JSONStoreConfig) *Store { return &Store{config: *c} } +func (store *Store) IsSingleValueStore() bool { + return false +} + +func (store *Store) Name() string { + return "json" +} + // BinaryStore handles storage of binary data in a JSON envelope. type BinaryStore struct { store Store config config.JSONBinaryStoreConfig } +// The binary store uses a single key ("data") to store everything. +func (store *BinaryStore) IsSingleValueStore() bool { + return true +} + +func (store *BinaryStore) Name() string { + return "binary" +} + func NewBinaryStore(c *config.JSONBinaryStoreConfig) *BinaryStore { return &BinaryStore{config: *c, store: *NewStore(&config.JSONStoreConfig{ Indent: c.Indent, diff --git a/stores/yaml/store.go b/stores/yaml/store.go index 3b15ae1a7..639e6867d 100644 --- a/stores/yaml/store.go +++ b/stores/yaml/store.go @@ -24,6 +24,14 @@ func NewStore(c *config.YAMLStoreConfig) *Store { return &Store{config: *c} } +func (store *Store) IsSingleValueStore() bool { + return false +} + +func (store *Store) Name() string { + return "yaml" +} + func (store Store) appendCommentToList(comment string, list []interface{}) []interface{} { if comment != "" { for _, commentLine := range strings.Split(comment, "\n") {