From baf25ca66decefbec82aa9dd11864d915ba1b93b Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 6 Mar 2026 21:08:02 +0100 Subject: [PATCH 1/3] Uniformly check file before encrypting. Signed-off-by: Felix Fontein --- cmd/sops/edit.go | 18 ++++++++++++++++-- cmd/sops/encrypt.go | 30 ++++++++++++++++++++---------- cmd/sops/set.go | 4 ++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 3510441533..f5c7191bff 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -39,6 +39,7 @@ type runEditorUntilOkOpts struct { TmpFileName string OriginalHash []byte InputStore sops.Store + OutputStore common.Store ShowMasterKeys bool Tree *sops.Tree } @@ -147,8 +148,12 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { // Let the user edit the file err = runEditorUntilOk(runEditorUntilOkOpts{ - InputStore: opts.InputStore, OriginalHash: origHash, TmpFileName: tmpfileName, - ShowMasterKeys: opts.ShowMasterKeys, Tree: tree}) + InputStore: opts.InputStore, + OutputStore: opts.OutputStore, + OriginalHash: origHash, + TmpFileName: tmpfileName, + ShowMasterKeys: opts.ShowMasterKeys, + Tree: tree}) if err != nil { return nil, err } @@ -213,6 +218,15 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { // Replace the whole tree, because otherwise newBranches would // contain the SOPS metadata opts.Tree = &t + } else { + if userErr, _ := validateFileForEncryption(opts.OutputStore, newBranches); userErr != nil { + log.WithField( + "error", + userErr.UserError(), + ).Errorf("Tree not valid for encryption. Press a key to return to the editor, or Ctrl+C to exit.") + bufio.NewReader(os.Stdin).ReadByte() + continue + } } opts.Tree.Branches = newBranches needVersionUpdated, err := version.AIsNewerThanB(version.Version, opts.Tree.Metadata.Version) diff --git a/cmd/sops/encrypt.go b/cmd/sops/encrypt.go index e5ffc6950e..cabba0fd42 100644 --- a/cmd/sops/encrypt.go +++ b/cmd/sops/encrypt.go @@ -52,15 +52,28 @@ func (err *fileAlreadyEncryptedError) UserError() string { "encrypt files that already contain such an entry.\n\n" + "If this is an unencrypted file, rename the '" + stores.SopsMetadataKey + "' entry.\n\n" + "If this is an encrypted file and you want to edit it, use the " + - "editor mode, for example: `sops my_file.yaml`" + "editor mode, for example: `sops edit my_file.yaml`" return wordwrap.WrapString(message, 75) } -func ensureNoMetadata(opts encryptOpts, branch sops.TreeBranch) error { - if opts.OutputStore.HasSopsTopLevelKey(branch) { - return &fileAlreadyEncryptedError{} +type needAtLeastOneDocument struct{} + +func (err *needAtLeastOneDocument) Error() string { + return "Empty file" +} + +func (err *needAtLeastOneDocument) UserError() string { + return "File cannot be completely empty, it must contain at least one document" +} + +func validateFileForEncryption(outputStore sops.Store, branches []sops.TreeBranch) (sops.UserError, int) { + if len(branches) < 1 { + return &needAtLeastOneDocument{}, codes.NeedAtLeastOneDocument + } + if outputStore.HasSopsTopLevelKey(branches[0]) { + return &fileAlreadyEncryptedError{}, codes.FileAlreadyEncrypted } - return nil + return nil, 0 } func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata { @@ -96,11 +109,8 @@ func encrypt(opts encryptOpts) (encryptedFile []byte, err error) { if err != nil { return nil, common.NewExitError(fmt.Sprintf("Error unmarshalling file: %s", err), codes.CouldNotReadInputFile) } - if len(branches) < 1 { - return nil, common.NewExitError("File cannot be completely empty, it must contain at least one document", codes.NeedAtLeastOneDocument) - } - if err := ensureNoMetadata(opts, branches[0]); err != nil { - return nil, common.NewExitError(err, codes.FileAlreadyEncrypted) + if err, code := validateFileForEncryption(opts.OutputStore, branches); err != nil { + return nil, common.NewExitError(err, code) } path, err := filepath.Abs(opts.InputPath) if err != nil { diff --git a/cmd/sops/set.go b/cmd/sops/set.go index 7a7da298df..7c56f77757 100644 --- a/cmd/sops/set.go +++ b/cmd/sops/set.go @@ -51,6 +51,10 @@ func set(opts setOpts) ([]byte, bool, error) { var changed bool tree.Branches[0], changed = tree.Branches[0].Set(opts.TreePath, opts.Value) + if err, code := validateFileForEncryption(opts.OutputStore, tree.Branches); err != nil { + return nil, false, common.NewExitError(err, code) + } + err = common.EncryptTree(common.EncryptTreeOpts{ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher, }) From 1d8c22600b08141fada1465e7f7a4ff5ce5460be Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 6 Mar 2026 21:10:36 +0100 Subject: [PATCH 2/3] Refactor loop. Signed-off-by: Felix Fontein --- cmd/sops/edit.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index f5c7191bff..8b6bfd4270 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -174,6 +174,12 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { return encryptedFile, nil } +const pressKeyMsg = "Press a key to return to the editor, or Ctrl+C to exit." + +func waitForKeyPress() { + bufio.NewReader(os.Stdin).ReadByte() +} + func runEditorUntilOk(opts runEditorUntilOkOpts) error { for { err := runEditor(opts.TmpFileName) @@ -196,10 +202,8 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { log.WithField( "error", err, - ).Errorf("Could not load tree, probably due to invalid " + - "syntax. Press a key to return to the editor, or Ctrl+C to " + - "exit.") - bufio.NewReader(os.Stdin).ReadByte() + ).Errorf("Could not load tree, probably due to invalid syntax. " + pressKeyMsg) + waitForKeyPress() continue } if opts.ShowMasterKeys { @@ -210,9 +214,8 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { log.WithField( "error", err, - ).Errorf("SOPS metadata is invalid. Press a key to " + - "return to the editor, or Ctrl+C to exit.") - bufio.NewReader(os.Stdin).ReadByte() + ).Errorf("SOPS metadata is invalid. " + pressKeyMsg) + waitForKeyPress() continue } // Replace the whole tree, because otherwise newBranches would @@ -223,8 +226,8 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { log.WithField( "error", userErr.UserError(), - ).Errorf("Tree not valid for encryption. Press a key to return to the editor, or Ctrl+C to exit.") - bufio.NewReader(os.Stdin).ReadByte() + ).Errorf("Tree not valid for encryption. " + pressKeyMsg) + waitForKeyPress() continue } } @@ -237,10 +240,8 @@ func runEditorUntilOk(opts runEditorUntilOkOpts) error { opts.Tree.Metadata.Version = version.Version } if opts.Tree.Metadata.MasterKeyCount() == 0 { - log.Error("No master keys were provided, so sops can't " + - "encrypt the file. Press a key to return to the editor, or " + - "Ctrl+C to exit.") - bufio.NewReader(os.Stdin).ReadByte() + log.Error("No master keys were provided, so sops can't encrypt the file. " + pressKeyMsg) + waitForKeyPress() continue } break From 4419fa1962930ab468ee11561137405b8f750fa3 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 6 Mar 2026 21:13:20 +0100 Subject: [PATCH 3/3] At least on Linux, you need to press Enter. Signed-off-by: Felix Fontein --- cmd/sops/edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sops/edit.go b/cmd/sops/edit.go index 8b6bfd4270..ef30fe3703 100644 --- a/cmd/sops/edit.go +++ b/cmd/sops/edit.go @@ -174,7 +174,7 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) { return encryptedFile, nil } -const pressKeyMsg = "Press a key to return to the editor, or Ctrl+C to exit." +const pressKeyMsg = "Press enter to return to the editor, or Ctrl+C to exit." func waitForKeyPress() { bufio.NewReader(os.Stdin).ReadByte()