Skip to content

Commit 0b358db

Browse files
committed
Allow to encrypt and decrypt from stdin.
Signed-off-by: Felix Fontein <felix@fontein.de>
1 parent 4bc6aa6 commit 0b358db

File tree

6 files changed

+170
-54
lines changed

6 files changed

+170
-54
lines changed

README.rst

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -359,33 +359,33 @@ Encrypting and decrypting from other programs
359359
When using ``sops`` in scripts or from other programs, there are often situations where you do not want to write
360360
encrypted or decrypted data to disk. The best way to avoid this is to pass data to SOPS via stdin, and to let
361361
SOPS write data to stdout. By default, the encrypt and decrypt operations write data to stdout already. To pass
362-
data via stdin, you need to pass ``/dev/stdin`` as the input filename. Please note that this only works on
363-
Unix-like operating systems such as macOS and Linux. On Windows, you have to use named pipes.
362+
data via stdin, you need to not provide an input filename. For encrpytion, you also must provide the
363+
``--filename-override`` option with the file's filename. The filename will be used to determine the input and output
364+
types, and to select the correct creation rule.
364365

365366
To decrypt data, you can simply do:
366367

367368
.. code:: sh
368369
369-
$ cat encrypted-data | sops decrypt /dev/stdin > decrypted-data
370+
$ cat encrypted-data | sops decrypt --filename-override filename.yaml > decrypted-data
370371
371372
To control the input and output format, pass ``--input-type`` and ``--output-type`` as appropriate. By default,
372-
``sops`` determines the input and output format from the provided filename, which is ``/dev/stdin`` here, and
373+
``sops`` determines the input and output format from the provided filename, which is the empty string here, and
373374
thus will use the binary store which expects JSON input and outputs binary data on decryption.
374375

375376
For example, to decrypt YAML data and obtain the decrypted result as YAML, use:
376377

377378
.. code:: sh
378379
379-
$ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml /dev/stdin > decrypted-data
380+
$ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml > decrypted-data
380381
381382
To encrypt, it is important to note that SOPS also uses the filename to look up the correct creation rule from
382-
``.sops.yaml``. Likely ``/dev/stdin`` will not match a creation rule, or only match the fallback rule without
383-
``path_regex``, which is usually not what you want. For that, ``sops`` provides the ``--filename-override``
384-
parameter which allows you to tell SOPS which filename to use to match creation rules:
383+
``.sops.yaml``. Therefore, you must provide the ``--filename-override`` parameter which allows you to tell
384+
SOPS which filename to use to match creation rules:
385385

386386
.. code:: sh
387387
388-
$ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml /dev/stdin > encrypted-data
388+
$ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml > encrypted-data
389389
390390
SOPS will find a matching creation rule for ``path/filename.sops.yaml`` in ``.sops.yaml`` and use that one to
391391
encrypt the data from stdin. This filename will also be used to determine the input and output store. As always,
@@ -394,7 +394,7 @@ the input store type can be adjusted by passing ``--input-type``, and the output
394394

395395
.. code:: sh
396396
397-
$ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv /dev/stdin > encrypted-data
397+
$ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv > encrypted-data
398398
399399
400400
Encrypting using Hashicorp Vault
@@ -1237,7 +1237,7 @@ When operating on stdin, use the ``--input-type`` and ``--output-type`` flags as
12371237
12381238
.. code:: sh
12391239
1240-
$ cat myfile.json | sops decrypt --input-type json --output-type json /dev/stdin
1240+
$ cat myfile.json | sops decrypt --input-type json --output-type json
12411241
12421242
JSON and JSON_binary indentation
12431243
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

cmd/sops/common/common.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package common
22

33
import (
44
"fmt"
5+
"io"
56
"os"
67
"path/filepath"
78
"time"
@@ -130,11 +131,20 @@ func EncryptTree(opts EncryptTreeOpts) error {
130131
return nil
131132
}
132133

133-
// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree
134-
func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) {
135-
fileBytes, err := os.ReadFile(inputPath)
136-
if err != nil {
137-
return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile)
134+
// LoadEncryptedFileEx loads an encrypted SOPS file from a file or stdin, returning a SOPS tree
135+
func LoadEncryptedFileEx(loader sops.EncryptedFileLoader, inputPath string, readFromStdin bool) (*sops.Tree, error) {
136+
var fileBytes []byte
137+
var err error
138+
if readFromStdin {
139+
fileBytes, err = io.ReadAll(os.Stdin)
140+
if err != nil {
141+
return nil, NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile)
142+
}
143+
} else {
144+
fileBytes, err = os.ReadFile(inputPath)
145+
if err != nil {
146+
return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile)
147+
}
138148
}
139149
path, err := filepath.Abs(inputPath)
140150
if err != nil {
@@ -145,6 +155,11 @@ func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops
145155
return &tree, err
146156
}
147157

158+
// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree
159+
func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) {
160+
return LoadEncryptedFileEx(loader, inputPath, false)
161+
}
162+
148163
// NewExitError returns a cli.ExitError given an error (wrapped in a generic interface{})
149164
// and an exit code to represent the failure
150165
func NewExitError(i interface{}, exitCode int) *cli.ExitError {
@@ -227,6 +242,7 @@ type GenericDecryptOpts struct {
227242
Cipher sops.Cipher
228243
InputStore sops.Store
229244
InputPath string
245+
ReadFromStdin bool
230246
IgnoreMAC bool
231247
KeyServices []keyservice.KeyServiceClient
232248
DecryptionOrder []string
@@ -235,7 +251,7 @@ type GenericDecryptOpts struct {
235251
// LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes
236252
// check for the issue described in https://github.com/mozilla/sops/pull/435
237253
func LoadEncryptedFileWithBugFixes(opts GenericDecryptOpts) (*sops.Tree, error) {
238-
tree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath)
254+
tree, err := LoadEncryptedFileEx(opts.InputStore, opts.InputPath, opts.ReadFromStdin)
239255
if err != nil {
240256
return nil, err
241257
}

cmd/sops/decrypt.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type decryptOpts struct {
1919
InputStore sops.Store
2020
OutputStore sops.Store
2121
InputPath string
22+
ReadFromStdin bool
2223
IgnoreMAC bool
2324
Extract []interface{}
2425
KeyServices []keyservice.KeyServiceClient
@@ -27,11 +28,12 @@ type decryptOpts struct {
2728

2829
func decryptTree(opts decryptOpts) (tree *sops.Tree, err error) {
2930
tree, err = common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
30-
Cipher: opts.Cipher,
31-
InputStore: opts.InputStore,
32-
InputPath: opts.InputPath,
33-
IgnoreMAC: opts.IgnoreMAC,
34-
KeyServices: opts.KeyServices,
31+
Cipher: opts.Cipher,
32+
InputStore: opts.InputStore,
33+
InputPath: opts.InputPath,
34+
ReadFromStdin: opts.ReadFromStdin,
35+
IgnoreMAC: opts.IgnoreMAC,
36+
KeyServices: opts.KeyServices,
3537
})
3638
if err != nil {
3739
return nil, err

cmd/sops/encrypt.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"io"
56
"os"
67
"path/filepath"
78

@@ -27,11 +28,12 @@ type encryptConfig struct {
2728
}
2829

2930
type encryptOpts struct {
30-
Cipher sops.Cipher
31-
InputStore sops.Store
32-
OutputStore sops.Store
33-
InputPath string
34-
KeyServices []keyservice.KeyServiceClient
31+
Cipher sops.Cipher
32+
InputStore sops.Store
33+
OutputStore sops.Store
34+
InputPath string
35+
ReadFromStdin bool
36+
KeyServices []keyservice.KeyServiceClient
3537
encryptConfig
3638
}
3739

@@ -78,9 +80,17 @@ func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata {
7880

7981
func encrypt(opts encryptOpts) (encryptedFile []byte, err error) {
8082
// Load the file
81-
fileBytes, err := os.ReadFile(opts.InputPath)
82-
if err != nil {
83-
return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile)
83+
var fileBytes []byte
84+
if opts.ReadFromStdin {
85+
fileBytes, err = io.ReadAll(os.Stdin)
86+
if err != nil {
87+
return nil, common.NewExitError(fmt.Sprintf("Error reading from stdin: %s", err), codes.CouldNotReadInputFile)
88+
}
89+
} else {
90+
fileBytes, err = os.ReadFile(opts.InputPath)
91+
if err != nil {
92+
return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile)
93+
}
8494
}
8595
branches, err := opts.InputStore.LoadPlainFile(fileBytes)
8696
if err != nil {

cmd/sops/main.go

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,8 @@ func main() {
707707
},
708708
{
709709
Name: "decrypt",
710-
Usage: "decrypt a file, and output the results to stdout",
711-
ArgsUsage: `file`,
710+
Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.",
711+
ArgsUsage: `[file]`,
712712
Flags: append([]cli.Flag{
713713
cli.BoolFlag{
714714
Name: "in-place, i",
@@ -736,7 +736,7 @@ func main() {
736736
},
737737
cli.StringFlag{
738738
Name: "filename-override",
739-
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
739+
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.",
740740
},
741741
cli.StringFlag{
742742
Name: "decryption-order",
@@ -748,19 +748,24 @@ func main() {
748748
if c.Bool("verbose") {
749749
logging.SetLevel(logrus.DebugLevel)
750750
}
751-
if c.NArg() < 1 {
752-
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
751+
if c.NArg() == 0 && c.Bool("in-place") {
752+
return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters)
753753
}
754754
warnMoreThanOnePositionalArgument(c)
755755
if c.Bool("in-place") && c.String("output") != "" {
756756
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
757757
}
758-
fileName, err := filepath.Abs(c.Args()[0])
759-
if err != nil {
760-
return toExitError(err)
761-
}
762-
if _, err := os.Stat(fileName); os.IsNotExist(err) {
763-
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
758+
readFromStdin := c.NArg() == 0
759+
var fileName string
760+
var err error
761+
if !readFromStdin {
762+
fileName, err = filepath.Abs(c.Args()[0])
763+
if err != nil {
764+
return toExitError(err)
765+
}
766+
if _, err := os.Stat(fileName); os.IsNotExist(err) {
767+
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
768+
}
764769
}
765770
fileNameOverride := c.String("filename-override")
766771
if fileNameOverride == "" {
@@ -791,6 +796,7 @@ func main() {
791796
OutputStore: outputStore,
792797
InputStore: inputStore,
793798
InputPath: fileName,
799+
ReadFromStdin: readFromStdin,
794800
Cipher: aes.NewCipher(),
795801
Extract: extract,
796802
KeyServices: svcs,
@@ -832,8 +838,8 @@ func main() {
832838
},
833839
{
834840
Name: "encrypt",
835-
Usage: "encrypt a file, and output the results to stdout",
836-
ArgsUsage: `file`,
841+
Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.",
842+
ArgsUsage: `[file]`,
837843
Flags: append([]cli.Flag{
838844
cli.BoolFlag{
839845
Name: "in-place, i",
@@ -911,26 +917,36 @@ func main() {
911917
},
912918
cli.StringFlag{
913919
Name: "filename-override",
914-
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
920+
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.",
915921
},
916922
}, keyserviceFlags...),
917923
Action: func(c *cli.Context) error {
918924
if c.Bool("verbose") {
919925
logging.SetLevel(logrus.DebugLevel)
920926
}
921-
if c.NArg() < 1 {
922-
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
927+
if c.NArg() == 0 {
928+
if c.Bool("in-place") {
929+
return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters)
930+
}
931+
if c.String("filename-override") == "" {
932+
return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters)
933+
}
923934
}
924935
warnMoreThanOnePositionalArgument(c)
925936
if c.Bool("in-place") && c.String("output") != "" {
926937
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
927938
}
928-
fileName, err := filepath.Abs(c.Args()[0])
929-
if err != nil {
930-
return toExitError(err)
931-
}
932-
if _, err := os.Stat(fileName); os.IsNotExist(err) {
933-
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
939+
readFromStdin := c.NArg() == 0
940+
var fileName string
941+
var err error
942+
if !readFromStdin {
943+
fileName, err = filepath.Abs(c.Args()[0])
944+
if err != nil {
945+
return toExitError(err)
946+
}
947+
if _, err := os.Stat(fileName); os.IsNotExist(err) {
948+
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
949+
}
934950
}
935951
fileNameOverride := c.String("filename-override")
936952
if fileNameOverride == "" {
@@ -955,6 +971,7 @@ func main() {
955971
OutputStore: outputStore,
956972
InputStore: inputStore,
957973
InputPath: fileName,
974+
ReadFromStdin: readFromStdin,
958975
Cipher: aes.NewCipher(),
959976
KeyServices: svcs,
960977
encryptConfig: encConfig,

0 commit comments

Comments
 (0)