Skip to content

Commit 81d31c5

Browse files
committed
Allow to encrypt and decrypt from stdin.
Signed-off-by: Felix Fontein <felix@fontein.de>
1 parent c78b0aa commit 81d31c5

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
@@ -376,33 +376,33 @@ Encrypting and decrypting from other programs
376376
When using ``sops`` in scripts or from other programs, there are often situations where you do not want to write
377377
encrypted or decrypted data to disk. The best way to avoid this is to pass data to SOPS via stdin, and to let
378378
SOPS write data to stdout. By default, the encrypt and decrypt operations write data to stdout already. To pass
379-
data via stdin, you need to pass ``/dev/stdin`` as the input filename. Please note that this only works on
380-
Unix-like operating systems such as macOS and Linux. On Windows, you have to use named pipes.
379+
data via stdin, you need to not provide an input filename. For encrpytion, you also must provide the
380+
``--filename-override`` option with the file's filename. The filename will be used to determine the input and output
381+
types, and to select the correct creation rule.
381382

382383
To decrypt data, you can simply do:
383384

384385
.. code:: sh
385386
386-
$ cat encrypted-data | sops decrypt /dev/stdin > decrypted-data
387+
$ cat encrypted-data | sops decrypt --filename-override filename.yaml > decrypted-data
387388
388389
To control the input and output format, pass ``--input-type`` and ``--output-type`` as appropriate. By default,
389-
``sops`` determines the input and output format from the provided filename, which is ``/dev/stdin`` here, and
390+
``sops`` determines the input and output format from the provided filename, which is the empty string here, and
390391
thus will use the binary store which expects JSON input and outputs binary data on decryption.
391392

392393
For example, to decrypt YAML data and obtain the decrypted result as YAML, use:
393394

394395
.. code:: sh
395396
396-
$ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml /dev/stdin > decrypted-data
397+
$ cat encrypted-data | sops decrypt --input-type yaml --output-type yaml > decrypted-data
397398
398399
To encrypt, it is important to note that SOPS also uses the filename to look up the correct creation rule from
399-
``.sops.yaml``. Likely ``/dev/stdin`` will not match a creation rule, or only match the fallback rule without
400-
``path_regex``, which is usually not what you want. For that, ``sops`` provides the ``--filename-override``
401-
parameter which allows you to tell SOPS which filename to use to match creation rules:
400+
``.sops.yaml``. Therefore, you must provide the ``--filename-override`` parameter which allows you to tell
401+
SOPS which filename to use to match creation rules:
402402

403403
.. code:: sh
404404
405-
$ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml /dev/stdin > encrypted-data
405+
$ echo 'foo: bar' | sops encrypt --filename-override path/filename.sops.yaml > encrypted-data
406406
407407
SOPS will find a matching creation rule for ``path/filename.sops.yaml`` in ``.sops.yaml`` and use that one to
408408
encrypt the data from stdin. This filename will also be used to determine the input and output store. As always,
@@ -411,7 +411,7 @@ the input store type can be adjusted by passing ``--input-type``, and the output
411411

412412
.. code:: sh
413413
414-
$ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv /dev/stdin > encrypted-data
414+
$ echo foo=bar | sops encrypt --filename-override path/filename.sops.yaml --input-type dotenv > encrypted-data
415415
416416
417417
Encrypting using Hashicorp Vault
@@ -1259,7 +1259,7 @@ When operating on stdin, use the ``--input-type`` and ``--output-type`` flags as
12591259
12601260
.. code:: sh
12611261
1262-
$ cat myfile.json | sops decrypt --input-type json --output-type json /dev/stdin
1262+
$ cat myfile.json | sops decrypt --input-type json --output-type json
12631263
12641264
JSON and JSON_binary indentation
12651265
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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
@@ -722,8 +722,8 @@ func main() {
722722
},
723723
{
724724
Name: "decrypt",
725-
Usage: "decrypt a file, and output the results to stdout",
726-
ArgsUsage: `file`,
725+
Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.",
726+
ArgsUsage: `[file]`,
727727
Flags: append([]cli.Flag{
728728
cli.BoolFlag{
729729
Name: "in-place, i",
@@ -751,7 +751,7 @@ func main() {
751751
},
752752
cli.StringFlag{
753753
Name: "filename-override",
754-
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
754+
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.",
755755
},
756756
cli.StringFlag{
757757
Name: "decryption-order",
@@ -763,19 +763,24 @@ func main() {
763763
if c.Bool("verbose") {
764764
logging.SetLevel(logrus.DebugLevel)
765765
}
766-
if c.NArg() < 1 {
767-
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
766+
if c.NArg() == 0 && c.Bool("in-place") {
767+
return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters)
768768
}
769769
warnMoreThanOnePositionalArgument(c)
770770
if c.Bool("in-place") && c.String("output") != "" {
771771
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
772772
}
773-
fileName, err := filepath.Abs(c.Args()[0])
774-
if err != nil {
775-
return toExitError(err)
776-
}
777-
if _, err := os.Stat(fileName); os.IsNotExist(err) {
778-
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
773+
readFromStdin := c.NArg() == 0
774+
var fileName string
775+
var err error
776+
if !readFromStdin {
777+
fileName, err = filepath.Abs(c.Args()[0])
778+
if err != nil {
779+
return toExitError(err)
780+
}
781+
if _, err := os.Stat(fileName); os.IsNotExist(err) {
782+
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
783+
}
779784
}
780785
fileNameOverride := c.String("filename-override")
781786
if fileNameOverride == "" {
@@ -806,6 +811,7 @@ func main() {
806811
OutputStore: outputStore,
807812
InputStore: inputStore,
808813
InputPath: fileName,
814+
ReadFromStdin: readFromStdin,
809815
Cipher: aes.NewCipher(),
810816
Extract: extract,
811817
KeyServices: svcs,
@@ -847,8 +853,8 @@ func main() {
847853
},
848854
{
849855
Name: "encrypt",
850-
Usage: "encrypt a file, and output the results to stdout",
851-
ArgsUsage: `file`,
856+
Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.",
857+
ArgsUsage: `[file]`,
852858
Flags: append([]cli.Flag{
853859
cli.BoolFlag{
854860
Name: "in-place, i",
@@ -926,26 +932,36 @@ func main() {
926932
},
927933
cli.StringFlag{
928934
Name: "filename-override",
929-
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
935+
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.",
930936
},
931937
}, keyserviceFlags...),
932938
Action: func(c *cli.Context) error {
933939
if c.Bool("verbose") {
934940
logging.SetLevel(logrus.DebugLevel)
935941
}
936-
if c.NArg() < 1 {
937-
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
942+
if c.NArg() == 0 {
943+
if c.Bool("in-place") {
944+
return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters)
945+
}
946+
if c.String("filename-override") == "" {
947+
return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters)
948+
}
938949
}
939950
warnMoreThanOnePositionalArgument(c)
940951
if c.Bool("in-place") && c.String("output") != "" {
941952
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
942953
}
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)
954+
readFromStdin := c.NArg() == 0
955+
var fileName string
956+
var err error
957+
if !readFromStdin {
958+
fileName, err = filepath.Abs(c.Args()[0])
959+
if err != nil {
960+
return toExitError(err)
961+
}
962+
if _, err := os.Stat(fileName); os.IsNotExist(err) {
963+
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
964+
}
949965
}
950966
fileNameOverride := c.String("filename-override")
951967
if fileNameOverride == "" {
@@ -970,6 +986,7 @@ func main() {
970986
OutputStore: outputStore,
971987
InputStore: inputStore,
972988
InputPath: fileName,
989+
ReadFromStdin: readFromStdin,
973990
Cipher: aes.NewCipher(),
974991
KeyServices: svcs,
975992
encryptConfig: encConfig,

0 commit comments

Comments
 (0)