diff --git a/cmd/cmd/sops/codes/codes.go b/cmd/cmd/sops/codes/codes.go
new file mode 100644
index 000000000..d62605bd0
--- /dev/null
+++ b/cmd/cmd/sops/codes/codes.go
@@ -0,0 +1,33 @@
+// Package codes the exit statuses returned by the sops binary
+package codes
+
+// Exit statuses returned by the binary
+const (
+ ErrorGeneric int = 1
+ CouldNotReadInputFile int = 2
+ CouldNotWriteOutputFile int = 3
+ ErrorDumpingTree int = 4
+ ErrorReadingConfig int = 5
+ ErrorInvalidKMSEncryptionContextFormat int = 6
+ ErrorInvalidSetFormat int = 7
+ ErrorConflictingParameters int = 8
+ ErrorEncryptingMac int = 21
+ ErrorEncryptingTree int = 23
+ ErrorDecryptingMac int = 24
+ ErrorDecryptingTree int = 25
+ CannotChangeKeysFromNonExistentFile int = 49
+ MacMismatch int = 51
+ MacNotFound int = 52
+ ConfigFileNotFound int = 61
+ KeyboardInterrupt int = 85
+ InvalidTreePathFormat int = 91
+ NeedAtLeastOneDocument int = 92
+ NoFileSpecified int = 100
+ CouldNotRetrieveKey int = 128
+ NoEncryptionKeyFound int = 111
+ DuplicateDecryptionKeyType int = 112
+ FileHasNotBeenModified int = 200
+ NoEditorFound int = 201
+ FailedToCompareVersions int = 202
+ FileAlreadyEncrypted int = 203
+)
diff --git a/cmd/cmd/sops/common/common.go b/cmd/cmd/sops/common/common.go
new file mode 100644
index 000000000..eecd34423
--- /dev/null
+++ b/cmd/cmd/sops/common/common.go
@@ -0,0 +1,449 @@
+package common
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/fatih/color"
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ . "github.com/getsops/sops/v3/cmd/sops/formats"
+ "github.com/getsops/sops/v3/config"
+ "github.com/getsops/sops/v3/keys"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/kms"
+ "github.com/getsops/sops/v3/stores/dotenv"
+ "github.com/getsops/sops/v3/stores/ini"
+ "github.com/getsops/sops/v3/stores/json"
+ "github.com/getsops/sops/v3/stores/yaml"
+ "github.com/getsops/sops/v3/version"
+ "github.com/mitchellh/go-wordwrap"
+ "github.com/urfave/cli"
+ "golang.org/x/term"
+)
+
+// ExampleFileEmitter emits example files. This is used by the `sops` binary
+// whenever a new file is created, in order to present the user with a non-empty file
+type ExampleFileEmitter interface {
+ EmitExample() []byte
+}
+
+// Store handles marshaling and unmarshaling from SOPS files
+type Store interface {
+ sops.Store
+ ExampleFileEmitter
+}
+
+type storeConstructor = func(*config.StoresConfig) Store
+
+func newBinaryStore(c *config.StoresConfig) Store {
+ return json.NewBinaryStore(&c.JSONBinary)
+}
+
+func newDotenvStore(c *config.StoresConfig) Store {
+ return dotenv.NewStore(&c.Dotenv)
+}
+
+func newIniStore(c *config.StoresConfig) Store {
+ return ini.NewStore(&c.INI)
+}
+
+func newJsonStore(c *config.StoresConfig) Store {
+ return json.NewStore(&c.JSON)
+}
+
+func newYamlStore(c *config.StoresConfig) Store {
+ return yaml.NewStore(&c.YAML)
+}
+
+var storeConstructors = map[Format]storeConstructor{
+ Binary: newBinaryStore,
+ Dotenv: newDotenvStore,
+ Ini: newIniStore,
+ Json: newJsonStore,
+ Yaml: newYamlStore,
+}
+
+// DecryptTreeOpts are the options needed to decrypt a tree
+type DecryptTreeOpts struct {
+ // Tree is the tree to be decrypted
+ Tree *sops.Tree
+ // KeyServices are the key services to be used for decryption of the data key
+ KeyServices []keyservice.KeyServiceClient
+ // DecryptionOrder is the order in which available decryption methods are tried
+ DecryptionOrder []string
+ // IgnoreMac is whether or not to ignore the Message Authentication Code included in the SOPS tree
+ IgnoreMac bool
+ // Cipher is the cryptographic cipher to use to decrypt the values inside the tree
+ Cipher sops.Cipher
+}
+
+// DecryptTree decrypts the tree passed in through the DecryptTreeOpts and additionally returns the decrypted data key
+func DecryptTree(opts DecryptTreeOpts) (dataKey []byte, err error) {
+ dataKey, err = opts.Tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
+ if err != nil {
+ return nil, NewExitError(err, codes.CouldNotRetrieveKey)
+ }
+ computedMac, err := opts.Tree.Decrypt(dataKey, opts.Cipher)
+ if err != nil {
+ return nil, NewExitError(fmt.Sprintf("Error decrypting tree: %s", err), codes.ErrorDecryptingTree)
+ }
+ fileMac, err := opts.Cipher.Decrypt(opts.Tree.Metadata.MessageAuthenticationCode, dataKey, opts.Tree.Metadata.LastModified.Format(time.RFC3339))
+ if !opts.IgnoreMac {
+ if err != nil {
+ return nil, NewExitError(fmt.Sprintf("Cannot decrypt MAC: %s", err), codes.MacMismatch)
+ }
+ if fileMac != computedMac {
+ // If the file has an empty MAC, display "no MAC" instead of not displaying anything
+ if fileMac == "" {
+ fileMac = "no MAC"
+ }
+ return nil, NewExitError(fmt.Sprintf("MAC mismatch. File has %s, computed %s", fileMac, computedMac), codes.MacMismatch)
+ }
+ }
+ return dataKey, nil
+}
+
+// EncryptTreeOpts are the options needed to encrypt a tree
+type EncryptTreeOpts struct {
+ // Tree is the tree to be encrypted
+ Tree *sops.Tree
+ // Cipher is the cryptographic cipher to use to encrypt the values inside the tree
+ Cipher sops.Cipher
+ // DataKey is the key the cipher should use to encrypt the values inside the tree
+ DataKey []byte
+}
+
+// EncryptTree encrypts the tree passed in through the EncryptTreeOpts
+func EncryptTree(opts EncryptTreeOpts) error {
+ unencryptedMac, err := opts.Tree.Encrypt(opts.DataKey, opts.Cipher)
+ if err != nil {
+ return NewExitError(fmt.Sprintf("Error encrypting tree: %s", err), codes.ErrorEncryptingTree)
+ }
+ opts.Tree.Metadata.LastModified = time.Now().UTC()
+ opts.Tree.Metadata.MessageAuthenticationCode, err = opts.Cipher.Encrypt(unencryptedMac, opts.DataKey, opts.Tree.Metadata.LastModified.Format(time.RFC3339))
+ if err != nil {
+ return NewExitError(fmt.Sprintf("Could not encrypt MAC: %s", err), codes.ErrorEncryptingMac)
+ }
+ return nil
+}
+
+// LoadEncryptedFile loads an encrypted SOPS file, returning a SOPS tree
+func LoadEncryptedFile(loader sops.EncryptedFileLoader, inputPath string) (*sops.Tree, error) {
+ fileBytes, err := os.ReadFile(inputPath)
+ if err != nil {
+ return nil, NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile)
+ }
+ path, err := filepath.Abs(inputPath)
+ if err != nil {
+ return nil, err
+ }
+ tree, err := loader.LoadEncryptedFile(fileBytes)
+ tree.FilePath = path
+ return &tree, err
+}
+
+// NewExitError returns a cli.ExitError given an error (wrapped in a generic interface{})
+// and an exit code to represent the failure
+func NewExitError(i interface{}, exitCode int) *cli.ExitError {
+ if userErr, ok := i.(sops.UserError); ok {
+ return NewExitError(userErr.UserError(), exitCode)
+ }
+ return cli.NewExitError(i, exitCode)
+}
+
+// StoreForFormat returns the correct format-specific implementation
+// of the Store interface given the format.
+func StoreForFormat(format Format, c *config.StoresConfig) Store {
+ storeConst, found := storeConstructors[format]
+ if !found {
+ storeConst = storeConstructors[Binary] // default
+ }
+ return storeConst(c)
+}
+
+// DefaultStoreForPath returns the correct format-specific implementation
+// of the Store interface given the path to a file
+func DefaultStoreForPath(c *config.StoresConfig, path string) Store {
+ format := FormatForPath(path)
+ return StoreForFormat(format, c)
+}
+
+// DefaultStoreForPathOrFormat returns the correct format-specific implementation
+// of the Store interface given the formatString if specified, or the path to a file.
+// This is to support the cli, where both are provided.
+func DefaultStoreForPathOrFormat(c *config.StoresConfig, path string, format string) Store {
+ formatFmt := FormatForPathOrString(path, format)
+ return StoreForFormat(formatFmt, c)
+}
+
+// KMS_ENC_CTX_BUG_FIXED_VERSION represents the SOPS version in which the
+// encryption context bug was fixed
+const KMS_ENC_CTX_BUG_FIXED_VERSION = "3.3.0"
+
+// DetectKMSEncryptionContextBug returns true if the encryption context bug is detected
+// in a given runtime sops.Tree object
+func DetectKMSEncryptionContextBug(tree *sops.Tree) (bool, error) {
+ versionCheck, err := version.AIsNewerThanB(KMS_ENC_CTX_BUG_FIXED_VERSION, tree.Metadata.Version)
+ if err != nil {
+ return false, err
+ }
+
+ if versionCheck {
+ _, _, key := GetKMSKeyWithEncryptionCtx(tree)
+ if key != nil {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
+
+// GetKMSKeyWithEncryptionCtx returns the first KMS key affected by the encryption context bug as well as its location in the key groups.
+func GetKMSKeyWithEncryptionCtx(tree *sops.Tree) (keyGroupIndex int, keyIndex int, key *kms.MasterKey) {
+ for i, kg := range tree.Metadata.KeyGroups {
+ for n, k := range kg {
+ kmsKey, ok := k.(*kms.MasterKey)
+ if ok {
+ if kmsKey.EncryptionContext != nil && len(kmsKey.EncryptionContext) >= 2 {
+ duplicateValues := map[string]int{}
+ for _, v := range kmsKey.EncryptionContext {
+ duplicateValues[*v] = duplicateValues[*v] + 1
+ }
+ if len(duplicateValues) > 1 {
+ return i, n, kmsKey
+ }
+ }
+ }
+ }
+ }
+ return 0, 0, nil
+}
+
+// GenericDecryptOpts represents decryption options and config
+type GenericDecryptOpts struct {
+ Cipher sops.Cipher
+ InputStore sops.Store
+ InputPath string
+ IgnoreMAC bool
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+// LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes
+// check for the issue described in https://github.com/mozilla/sops/pull/435
+func LoadEncryptedFileWithBugFixes(opts GenericDecryptOpts) (*sops.Tree, error) {
+ tree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath)
+ if err != nil {
+ return nil, err
+ }
+
+ encCtxBug, err := DetectKMSEncryptionContextBug(tree)
+ if err != nil {
+ return nil, err
+ }
+ if encCtxBug {
+ tree, err = FixAWSKMSEncryptionContextBug(opts, tree)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return tree, nil
+}
+
+// FixAWSKMSEncryptionContextBug is used to fix the issue described in https://github.com/mozilla/sops/pull/435
+func FixAWSKMSEncryptionContextBug(opts GenericDecryptOpts, tree *sops.Tree) (*sops.Tree, error) {
+ message := "Up until version 3.3.0 of sops there was a bug surrounding the " +
+ "use of encryption context with AWS KMS." +
+ "\nYou can read the full description of the issue here:" +
+ "\nhttps://github.com/mozilla/sops/pull/435" +
+ "\n\nIf a TTY is detected, sops will ask you if you'd like for this issue to be " +
+ "automatically fixed, which will require re-encrypting the data keys used by " +
+ "each key." +
+ "\n\nIf you are not using a TTY, sops will fix the issue for this run.\n\n"
+ fmt.Println(wordwrap.WrapString(message, 75))
+
+ persistFix := false
+
+ if term.IsTerminal(int(os.Stdout.Fd())) {
+ var response string
+ for response != "y" && response != "n" {
+ fmt.Println("Would you like sops to automatically fix this issue? (y/n): ")
+ _, err := fmt.Scanln(&response)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if response == "n" {
+ return nil, fmt.Errorf("Exiting. User responded no")
+ }
+ persistFix = true
+ }
+
+ // If there is another key, then we should be able to just decrypt
+ // without having to try different variations of the encryption context.
+ dataKey, err := DecryptTree(DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ })
+ if err != nil {
+ dataKey = RecoverDataKeyFromBuggyKMS(opts, tree)
+ }
+
+ if dataKey == nil {
+ return nil, NewExitError(fmt.Sprintf("Failed to decrypt, meaning there is likely another problem from the encryption context bug: %s", err), codes.ErrorDecryptingTree)
+ }
+
+ errs := tree.Metadata.UpdateMasterKeysWithKeyServices(dataKey, opts.KeyServices)
+ if len(errs) > 0 {
+ err = fmt.Errorf("Could not re-encrypt data key: %s", errs)
+ return nil, err
+ }
+
+ err = EncryptTree(EncryptTreeOpts{
+ DataKey: dataKey,
+ Tree: tree,
+ Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // If we are not going to persist the fix, just return the re-encrypted tree.
+ if !persistFix {
+ return tree, nil
+ }
+
+ encryptedFile, err := opts.InputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return nil, NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+
+ file, err := os.Create(opts.InputPath)
+ if err != nil {
+ return nil, NewExitError(fmt.Sprintf("Could not open file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(encryptedFile)
+ if err != nil {
+ return nil, err
+ }
+
+ newTree, err := LoadEncryptedFile(opts.InputStore, opts.InputPath)
+ if err != nil {
+ return nil, err
+ }
+
+ return newTree, nil
+}
+
+// RecoverDataKeyFromBuggyKMS loops through variations on Encryption Context to
+// recover the datakey. This is used to fix the issue described in https://github.com/mozilla/sops/pull/435
+func RecoverDataKeyFromBuggyKMS(opts GenericDecryptOpts, tree *sops.Tree) []byte {
+ kgndx, kndx, originalKey := GetKMSKeyWithEncryptionCtx(tree)
+
+ keyToEdit := *originalKey
+
+ encCtxVals := map[string]interface{}{}
+ for _, v := range keyToEdit.EncryptionContext {
+ encCtxVals[*v] = ""
+ }
+
+ encCtxVariations := []map[string]*string{}
+ for ctxVal := range encCtxVals {
+ encCtxVariation := map[string]*string{}
+ for key := range keyToEdit.EncryptionContext {
+ val := ctxVal
+ encCtxVariation[key] = &val
+ }
+ encCtxVariations = append(encCtxVariations, encCtxVariation)
+ }
+
+ for _, encCtxVar := range encCtxVariations {
+ keyToEdit.EncryptionContext = encCtxVar
+ tree.Metadata.KeyGroups[kgndx][kndx] = &keyToEdit
+ dataKey, err := DecryptTree(DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ })
+ if err == nil {
+ tree.Metadata.KeyGroups[kgndx][kndx] = originalKey
+ tree.Metadata.Version = version.Version
+ return dataKey
+ }
+ }
+
+ return nil
+}
+
+// Diff represents a key diff
+type Diff struct {
+ Common []keys.MasterKey
+ Added []keys.MasterKey
+ Removed []keys.MasterKey
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+// DiffKeyGroups returns the list of diffs found in two sops.keyGroup slices
+func DiffKeyGroups(ours, theirs []sops.KeyGroup) []Diff {
+ var diffs []Diff
+ for i := 0; i < max(len(ours), len(theirs)); i++ {
+ var diff Diff
+ var ourGroup, theirGroup sops.KeyGroup
+ if len(ours) > i {
+ ourGroup = ours[i]
+ }
+ if len(theirs) > i {
+ theirGroup = theirs[i]
+ }
+ ourKeys := make(map[string]struct{})
+ theirKeys := make(map[string]struct{})
+ for _, key := range ourGroup {
+ ourKeys[key.ToString()] = struct{}{}
+ }
+ for _, key := range theirGroup {
+ if _, ok := ourKeys[key.ToString()]; ok {
+ diff.Common = append(diff.Common, key)
+ } else {
+ diff.Added = append(diff.Added, key)
+ }
+ theirKeys[key.ToString()] = struct{}{}
+ }
+ for _, key := range ourGroup {
+ if _, ok := theirKeys[key.ToString()]; !ok {
+ diff.Removed = append(diff.Removed, key)
+ }
+ }
+ diffs = append(diffs, diff)
+ }
+ return diffs
+}
+
+// PrettyPrintDiffs prints a slice of Diff objects to stdout
+func PrettyPrintDiffs(diffs []Diff) {
+ for i, diff := range diffs {
+ color.New(color.Underline).Printf("Group %d\n", i+1)
+ for _, c := range diff.Common {
+ fmt.Printf(" %s\n", c.ToString())
+ }
+ for _, c := range diff.Added {
+ color.New(color.FgGreen).Printf("+++ %s\n", c.ToString())
+ }
+ for _, c := range diff.Removed {
+ color.New(color.FgRed).Printf("--- %s\n", c.ToString())
+ }
+ }
+}
diff --git a/cmd/cmd/sops/decrypt.go b/cmd/cmd/sops/decrypt.go
new file mode 100644
index 000000000..db038787b
--- /dev/null
+++ b/cmd/cmd/sops/decrypt.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/stores/json"
+)
+
+const notBinaryHint = ("This is likely not an encrypted binary file?" +
+ " If not, use --output-type to select the correct output type.")
+
+type decryptOpts struct {
+ Cipher sops.Cipher
+ InputStore sops.Store
+ OutputStore sops.Store
+ InputPath string
+ IgnoreMAC bool
+ Extract []interface{}
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+func decryptTree(opts decryptOpts) (tree *sops.Tree, err error) {
+ tree, err = common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
+ Cipher: opts.Cipher,
+ InputStore: opts.InputStore,
+ InputPath: opts.InputPath,
+ IgnoreMAC: opts.IgnoreMAC,
+ KeyServices: opts.KeyServices,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return tree, nil
+}
+
+func decrypt(opts decryptOpts) (decryptedFile []byte, err error) {
+ tree, err := decryptTree(opts)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(opts.Extract) > 0 {
+ return extract(tree, opts.Extract, opts.OutputStore)
+ }
+ decryptedFile, err = opts.OutputStore.EmitPlainFile(tree.Branches)
+ if errors.Is(err, json.BinaryStoreEmitPlainError) {
+ err = fmt.Errorf("%s\n\n%s", err.Error(), notBinaryHint)
+ }
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Error dumping file: %s", err), codes.ErrorDumpingTree)
+ }
+ return decryptedFile, err
+}
+
+func extract(tree *sops.Tree, path []interface{}, outputStore sops.Store) (output []byte, err error) {
+ v, err := tree.Branches[0].Truncate(path)
+ if err != nil {
+ return nil, fmt.Errorf("error truncating tree: %s", err)
+ }
+ if newBranch, ok := v.(sops.TreeBranch); ok {
+ tree.Branches[0] = newBranch
+ decrypted, err := outputStore.EmitPlainFile(tree.Branches)
+ if errors.Is(err, json.BinaryStoreEmitPlainError) {
+ err = fmt.Errorf("%s\n\n%s", err.Error(), notBinaryHint)
+ }
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Error dumping file: %s", err), codes.ErrorDumpingTree)
+ }
+ return decrypted, err
+ } else if str, ok := v.(string); ok {
+ return []byte(str), nil
+ }
+ bytes, err := outputStore.EmitValue(v)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Error dumping tree: %s", err), codes.ErrorDumpingTree)
+ }
+ return bytes, nil
+}
diff --git a/cmd/cmd/sops/edit.go b/cmd/cmd/sops/edit.go
new file mode 100644
index 000000000..982cfb967
--- /dev/null
+++ b/cmd/cmd/sops/edit.go
@@ -0,0 +1,279 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/version"
+ "github.com/google/shlex"
+ exec "golang.org/x/sys/execabs"
+)
+
+type editOpts struct {
+ Cipher sops.Cipher
+ InputStore common.Store
+ OutputStore common.Store
+ InputPath string
+ IgnoreMAC bool
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+ ShowMasterKeys bool
+}
+
+type editExampleOpts struct {
+ editOpts
+ encryptConfig
+}
+
+type runEditorUntilOkOpts struct {
+ TmpFileName string
+ OriginalHash []byte
+ InputStore sops.Store
+ ShowMasterKeys bool
+ Tree *sops.Tree
+}
+
+func editExample(opts editExampleOpts) ([]byte, error) {
+ fileBytes := opts.InputStore.EmitExample()
+ branches, err := opts.InputStore.LoadPlainFile(fileBytes)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Error unmarshalling file: %s", err), codes.CouldNotReadInputFile)
+ }
+ path, err := filepath.Abs(opts.InputPath)
+ if err != nil {
+ return nil, err
+ }
+ tree := sops.Tree{
+ Branches: branches,
+ Metadata: metadataFromEncryptionConfig(opts.encryptConfig),
+ FilePath: path,
+ }
+
+ // Generate a data key
+ dataKey, errs := tree.GenerateDataKeyWithKeyServices(opts.KeyServices)
+ if len(errs) > 0 {
+ return nil, common.NewExitError(fmt.Sprintf("Error encrypting the data key with one or more master keys: %s", errs), codes.CouldNotRetrieveKey)
+ }
+
+ return editTree(opts.editOpts, &tree, dataKey)
+}
+
+func edit(opts editOpts) ([]byte, error) {
+ // Load the file
+ tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
+ Cipher: opts.Cipher,
+ InputStore: opts.InputStore,
+ InputPath: opts.InputPath,
+ IgnoreMAC: opts.IgnoreMAC,
+ KeyServices: opts.KeyServices,
+ })
+ if err != nil {
+ return nil, err
+ }
+ // Decrypt the file
+ dataKey, err := common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return editTree(opts, tree, dataKey)
+}
+
+func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) {
+ // Create temporary file for editing
+ tmpdir, err := os.MkdirTemp("", "")
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not create temporary directory: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer os.RemoveAll(tmpdir)
+
+ tmpfile, err := os.Create(filepath.Join(tmpdir, filepath.Base(opts.InputPath)))
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not create temporary file: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ // Ensure that in any case, the temporary file is always closed.
+ defer tmpfile.Close()
+
+ tmpfileName := tmpfile.Name()
+
+ // Write to temporary file
+ var out []byte
+ if opts.ShowMasterKeys {
+ out, err = opts.OutputStore.EmitEncryptedFile(*tree)
+ } else {
+ out, err = opts.OutputStore.EmitPlainFile(tree.Branches)
+ }
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ _, err = tmpfile.Write(out)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not write output file: %s", err), codes.CouldNotWriteOutputFile)
+ }
+
+ // Compute file hash to detect if the file has been edited
+ origHash, err := hashFile(tmpfileName)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not hash file: %s", err), codes.CouldNotReadInputFile)
+ }
+
+ // Close the temporary file, so that an editor can open it.
+ // We need to do this because some editors (e.g. VSCode) will refuse to
+ // open a file on Windows due to the Go standard library not opening
+ // files with shared delete access.
+ if err := tmpfile.Close(); err != nil {
+ return nil, err
+ }
+
+ // Let the user edit the file
+ err = runEditorUntilOk(runEditorUntilOkOpts{
+ InputStore: opts.InputStore, OriginalHash: origHash, TmpFileName: tmpfileName,
+ ShowMasterKeys: opts.ShowMasterKeys, Tree: tree})
+ if err != nil {
+ return nil, err
+ }
+
+ // Encrypt the file
+ err = common.EncryptTree(common.EncryptTreeOpts{
+ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Output the file
+ encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ return encryptedFile, nil
+}
+
+func runEditorUntilOk(opts runEditorUntilOkOpts) error {
+ for {
+ err := runEditor(opts.TmpFileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not run editor: %s", err), codes.NoEditorFound)
+ }
+ newHash, err := hashFile(opts.TmpFileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not hash file: %s", err), codes.CouldNotReadInputFile)
+ }
+ if bytes.Equal(newHash, opts.OriginalHash) {
+ return common.NewExitError("File has not changed, exiting.", codes.FileHasNotBeenModified)
+ }
+ edited, err := os.ReadFile(opts.TmpFileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not read edited file: %s", err), codes.CouldNotReadInputFile)
+ }
+ newBranches, err := opts.InputStore.LoadPlainFile(edited)
+ if err != nil {
+ 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()
+ continue
+ }
+ if opts.ShowMasterKeys {
+ // The file is not actually encrypted, but it contains SOPS
+ // metadata
+ t, err := opts.InputStore.LoadEncryptedFile(edited)
+ if err != nil {
+ 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()
+ continue
+ }
+ // Replace the whole tree, because otherwise newBranches would
+ // contain the SOPS metadata
+ opts.Tree = &t
+ }
+ opts.Tree.Branches = newBranches
+ needVersionUpdated, err := version.AIsNewerThanB(version.Version, opts.Tree.Metadata.Version)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Failed to compare document version %q with program version %q: %v", opts.Tree.Metadata.Version, version.Version, err), codes.FailedToCompareVersions)
+ }
+ if needVersionUpdated {
+ 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()
+ continue
+ }
+ break
+ }
+ return nil
+}
+
+func hashFile(filePath string) ([]byte, error) {
+ var result []byte
+ file, err := os.Open(filePath)
+ if err != nil {
+ return result, err
+ }
+ defer file.Close()
+ hash := sha256.New()
+ if _, err := io.Copy(hash, file); err != nil {
+ return result, err
+ }
+ return hash.Sum(result), nil
+}
+
+func runEditor(path string) error {
+ editor := os.Getenv("EDITOR")
+ var cmd *exec.Cmd
+ if editor == "" {
+ editor, err := lookupAnyEditor("vim", "nano", "vi")
+ if err != nil {
+ return err
+ }
+ cmd = exec.Command(editor, path)
+ } else {
+ parts, err := shlex.Split(editor)
+ if err != nil {
+ return fmt.Errorf("invalid $EDITOR: %s", editor)
+ }
+ parts = append(parts, path)
+ cmd = exec.Command(parts[0], parts[1:]...)
+ }
+
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+func lookupAnyEditor(editorNames ...string) (editorPath string, err error) {
+ for _, editorName := range editorNames {
+ editorPath, err = exec.LookPath(editorName)
+ if err == nil {
+ return editorPath, nil
+ }
+ }
+ return "", fmt.Errorf("no editor available: sops attempts to use the editor defined in the EDITOR environment variable, and if that's not set defaults to any of %s, but none of them could be found", strings.Join(editorNames, ", "))
+}
diff --git a/cmd/cmd/sops/encrypt.go b/cmd/cmd/sops/encrypt.go
new file mode 100644
index 000000000..ace7d8c2c
--- /dev/null
+++ b/cmd/cmd/sops/encrypt.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/stores"
+ "github.com/getsops/sops/v3/version"
+ "github.com/mitchellh/go-wordwrap"
+)
+
+type encryptConfig struct {
+ UnencryptedSuffix string
+ EncryptedSuffix string
+ UnencryptedRegex string
+ EncryptedRegex string
+ UnencryptedCommentRegex string
+ EncryptedCommentRegex string
+ MACOnlyEncrypted bool
+ KeyGroups []sops.KeyGroup
+ GroupThreshold int
+}
+
+type encryptOpts struct {
+ Cipher sops.Cipher
+ InputStore sops.Store
+ OutputStore sops.Store
+ InputPath string
+ KeyServices []keyservice.KeyServiceClient
+ encryptConfig
+}
+
+type fileAlreadyEncryptedError struct{}
+
+func (err *fileAlreadyEncryptedError) Error() string {
+ return "File already encrypted"
+}
+
+func (err *fileAlreadyEncryptedError) UserError() string {
+ message := "The file you have provided contains a top-level entry called " +
+ "'" + stores.SopsMetadataKey + "', or for flat file formats top-level entries starting with " +
+ "'" + stores.SopsMetadataKey + "_'. This is generally due to the file already being encrypted. " +
+ "SOPS uses a top-level entry called '" + stores.SopsMetadataKey + "' to store the metadata " +
+ "required to decrypt the file. For this reason, SOPS can not " +
+ "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`"
+ return wordwrap.WrapString(message, 75)
+}
+
+func ensureNoMetadata(opts encryptOpts, branch sops.TreeBranch) error {
+ if opts.OutputStore.HasSopsTopLevelKey(branch) {
+ return &fileAlreadyEncryptedError{}
+ }
+ return nil
+}
+
+func metadataFromEncryptionConfig(config encryptConfig) sops.Metadata {
+ return sops.Metadata{
+ KeyGroups: config.KeyGroups,
+ UnencryptedSuffix: config.UnencryptedSuffix,
+ EncryptedSuffix: config.EncryptedSuffix,
+ UnencryptedRegex: config.UnencryptedRegex,
+ EncryptedRegex: config.EncryptedRegex,
+ UnencryptedCommentRegex: config.UnencryptedCommentRegex,
+ EncryptedCommentRegex: config.EncryptedCommentRegex,
+ MACOnlyEncrypted: config.MACOnlyEncrypted,
+ Version: version.Version,
+ ShamirThreshold: config.GroupThreshold,
+ }
+}
+
+func encrypt(opts encryptOpts) (encryptedFile []byte, err error) {
+ // Load the file
+ fileBytes, err := os.ReadFile(opts.InputPath)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Error reading file: %s", err), codes.CouldNotReadInputFile)
+ }
+ branches, err := opts.InputStore.LoadPlainFile(fileBytes)
+ 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)
+ }
+ path, err := filepath.Abs(opts.InputPath)
+ if err != nil {
+ return nil, err
+ }
+ tree := sops.Tree{
+ Branches: branches,
+ Metadata: metadataFromEncryptionConfig(opts.encryptConfig),
+ FilePath: path,
+ }
+ dataKey, errs := tree.GenerateDataKeyWithKeyServices(opts.KeyServices)
+ if len(errs) > 0 {
+ err = fmt.Errorf("Could not generate data key: %s", errs)
+ return nil, err
+ }
+
+ err = common.EncryptTree(common.EncryptTreeOpts{
+ DataKey: dataKey,
+ Tree: &tree,
+ Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ encryptedFile, err = opts.OutputStore.EmitEncryptedFile(tree)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ return
+}
diff --git a/cmd/cmd/sops/formats/formats.go b/cmd/cmd/sops/formats/formats.go
new file mode 100644
index 000000000..83c5391e2
--- /dev/null
+++ b/cmd/cmd/sops/formats/formats.go
@@ -0,0 +1,78 @@
+package formats
+
+import "strings"
+
+// Format is an enum type
+type Format int
+
+const (
+ Binary Format = iota
+ Dotenv
+ Ini
+ Json
+ Yaml
+)
+
+var stringToFormat = map[string]Format{
+ "binary": Binary,
+ "dotenv": Dotenv,
+ "ini": Ini,
+ "json": Json,
+ "yaml": Yaml,
+}
+
+// FormatFromString returns a Format from a string.
+// This is used for converting string cli options.
+func FormatFromString(formatString string) Format {
+ format, found := stringToFormat[formatString]
+ if !found {
+ return Binary
+ }
+ return format
+}
+
+// IsYAMLFile returns true if a given file path corresponds to a YAML file
+func IsYAMLFile(path string) bool {
+ return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
+}
+
+// IsJSONFile returns true if a given file path corresponds to a JSON file
+func IsJSONFile(path string) bool {
+ return strings.HasSuffix(path, ".json")
+}
+
+// IsEnvFile returns true if a given file path corresponds to a .env file
+func IsEnvFile(path string) bool {
+ return strings.HasSuffix(path, ".env")
+}
+
+// IsIniFile returns true if a given file path corresponds to a INI file
+func IsIniFile(path string) bool {
+ return strings.HasSuffix(path, ".ini")
+}
+
+// FormatForPath returns the correct format given the path to a file
+func FormatForPath(path string) Format {
+ format := Binary // default
+ if IsYAMLFile(path) {
+ format = Yaml
+ } else if IsJSONFile(path) {
+ format = Json
+ } else if IsEnvFile(path) {
+ format = Dotenv
+ } else if IsIniFile(path) {
+ format = Ini
+ }
+ return format
+}
+
+// FormatForPathOrString returns the correct format-specific implementation
+// of the Store interface given the formatString if specified, or the path to a file.
+// This is to support the cli, where both are provided.
+func FormatForPathOrString(path, format string) Format {
+ formatFmt, found := stringToFormat[format]
+ if !found {
+ formatFmt = FormatForPath(path)
+ }
+ return formatFmt
+}
diff --git a/cmd/cmd/sops/formats/formats_test.go b/cmd/cmd/sops/formats/formats_test.go
new file mode 100644
index 000000000..7bc6477fa
--- /dev/null
+++ b/cmd/cmd/sops/formats/formats_test.go
@@ -0,0 +1,39 @@
+package formats
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFormatFromString(t *testing.T) {
+ assert.Equal(t, Binary, FormatFromString("foobar"))
+ assert.Equal(t, Dotenv, FormatFromString("dotenv"))
+ assert.Equal(t, Ini, FormatFromString("ini"))
+ assert.Equal(t, Yaml, FormatFromString("yaml"))
+ assert.Equal(t, Json, FormatFromString("json"))
+}
+
+func TestFormatForPath(t *testing.T) {
+ assert.Equal(t, Binary, FormatForPath("/path/to/foobar"))
+ assert.Equal(t, Dotenv, FormatForPath("/path/to/foobar.env"))
+ assert.Equal(t, Ini, FormatForPath("/path/to/foobar.ini"))
+ assert.Equal(t, Json, FormatForPath("/path/to/foobar.json"))
+ assert.Equal(t, Yaml, FormatForPath("/path/to/foobar.yml"))
+ assert.Equal(t, Yaml, FormatForPath("/path/to/foobar.yaml"))
+}
+
+func TestFormatForPathOrString(t *testing.T) {
+ assert.Equal(t, Binary, FormatForPathOrString("/path/to/foobar", ""))
+ assert.Equal(t, Dotenv, FormatForPathOrString("/path/to/foobar", "dotenv"))
+ assert.Equal(t, Dotenv, FormatForPathOrString("/path/to/foobar.env", ""))
+ assert.Equal(t, Ini, FormatForPathOrString("/path/to/foobar", "ini"))
+ assert.Equal(t, Ini, FormatForPathOrString("/path/to/foobar.ini", ""))
+ assert.Equal(t, Json, FormatForPathOrString("/path/to/foobar", "json"))
+ assert.Equal(t, Json, FormatForPathOrString("/path/to/foobar.json", ""))
+ assert.Equal(t, Yaml, FormatForPathOrString("/path/to/foobar", "yaml"))
+ assert.Equal(t, Yaml, FormatForPathOrString("/path/to/foobar.yml", ""))
+
+ assert.Equal(t, Ini, FormatForPathOrString("/path/to/foobar.yml", "ini"))
+ assert.Equal(t, Binary, FormatForPathOrString("/path/to/foobar.yml", "binary"))
+}
diff --git a/cmd/cmd/sops/main.go b/cmd/cmd/sops/main.go
new file mode 100644
index 000000000..155eaec78
--- /dev/null
+++ b/cmd/cmd/sops/main.go
@@ -0,0 +1,2276 @@
+package main // import "github.com/getsops/sops/v3/cmd/sops"
+
+import (
+ "context"
+ encodingjson "encoding/json"
+ "fmt"
+ "net"
+ "net/url"
+ "os"
+ osExec "os/exec"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/aes"
+ "github.com/getsops/sops/v3/age"
+ _ "github.com/getsops/sops/v3/audit"
+ "github.com/getsops/sops/v3/azkv"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/cmd/sops/subcommand/exec"
+ filestatuscmd "github.com/getsops/sops/v3/cmd/sops/subcommand/filestatus"
+ "github.com/getsops/sops/v3/cmd/sops/subcommand/groups"
+ keyservicecmd "github.com/getsops/sops/v3/cmd/sops/subcommand/keyservice"
+ publishcmd "github.com/getsops/sops/v3/cmd/sops/subcommand/publish"
+ "github.com/getsops/sops/v3/cmd/sops/subcommand/updatekeys"
+ "github.com/getsops/sops/v3/config"
+ "github.com/getsops/sops/v3/gcpkms"
+ "github.com/getsops/sops/v3/hcvault"
+ "github.com/getsops/sops/v3/keys"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/kms"
+ "github.com/getsops/sops/v3/logging"
+ "github.com/getsops/sops/v3/pgp"
+ "github.com/getsops/sops/v3/stores/dotenv"
+ "github.com/getsops/sops/v3/stores/json"
+ "github.com/getsops/sops/v3/version"
+)
+
+var log *logrus.Logger
+
+func init() {
+ log = logging.NewLogger("CMD")
+}
+
+func warnMoreThanOnePositionalArgument(c *cli.Context) {
+ if c.NArg() > 1 {
+ log.Warn("More than one positional argument provided. Only the first one will be used!")
+ potentialFlag := ""
+ for i, value := range c.Args() {
+ if i > 0 && strings.HasPrefix(value, "-") {
+ potentialFlag = value
+ }
+ }
+ if potentialFlag != "" {
+ log.Warn(fmt.Sprintf("Note that one of the ignored positional argument is %q, which looks like a flag. Flags must always be provided before the first positional argument!", potentialFlag))
+ }
+ }
+}
+
+func main() {
+ cli.VersionPrinter = version.PrintVersion
+ app := cli.NewApp()
+
+ keyserviceFlags := []cli.Flag{
+ cli.BoolTFlag{
+ Name: "enable-local-keyservice",
+ Usage: "use local key service",
+ },
+ cli.StringSliceFlag{
+ Name: "keyservice",
+ Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000",
+ },
+ }
+ app.Name = "sops"
+ app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, Azure Key Vault, age, and GPG support"
+ app.ArgsUsage = "sops [options] file"
+ app.Version = version.Version
+ app.Authors = []cli.Author{
+ {Name: "AJ Bahnken", Email: "ajvb@mozilla.com"},
+ {Name: "Adrian Utrilla", Email: "adrianutrilla@gmail.com"},
+ {Name: "Julien Vehent", Email: "jvehent@mozilla.com"},
+ }
+ app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, AZKV,
+ PGP, and Age
+
+ To encrypt or decrypt a document with AWS KMS, specify the KMS ARN
+ in the -k flag or in the SOPS_KMS_ARN environment variable.
+ (you need valid credentials in ~/.aws/credentials or in your env)
+
+ To encrypt or decrypt a document with GCP KMS, specify the
+ GCP KMS resource ID in the --gcp-kms flag or in the SOPS_GCP_KMS_IDS
+ environment variable.
+ (You need to setup Google application default credentials. See
+ https://developers.google.com/identity/protocols/application-default-credentials)
+
+
+ To encrypt or decrypt a document with HashiCorp Vault's Transit Secret
+ Engine, specify the Vault key URI name in the --hc-vault-transit flag
+ or in the SOPS_VAULT_URIS environment variable (for example
+ https://vault.example.org:8200/v1/transit/keys/dev, where
+ 'https://vault.example.org:8200' is the vault server, 'transit' the
+ enginePath, and 'dev' is the name of the key).
+ (You need to enable the Transit Secrets Engine in Vault. See
+ https://www.vaultproject.io/docs/secrets/transit/index.html)
+
+ To encrypt or decrypt a document with Azure Key Vault, specify the
+ Azure Key Vault key URL in the --azure-kv flag or in the
+ SOPS_AZURE_KEYVAULT_URL environment variable.
+ (Authentication is based on environment variables, see
+ https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication.
+ The user/sp needs the key/encrypt and key/decrypt permissions.)
+
+ To encrypt or decrypt using age, specify the recipient in the -a flag,
+ or in the SOPS_AGE_RECIPIENTS environment variable.
+
+ To encrypt or decrypt using PGP, specify the PGP fingerprint in the
+ -p flag or in the SOPS_PGP_FP environment variable.
+
+ To use multiple KMS or PGP keys, separate them by commas. For example:
+ $ sops -p "10F2...0A, 85D...B3F21" file.yaml
+
+ The -p, -k, --gcp-kms, --hc-vault-transit, and --azure-kv flags are only
+ used to encrypt new documents. Editing or decrypting existing documents
+ can be done with "sops file" or "sops decrypt file" respectively. The KMS and
+ PGP keys listed in the encrypted documents are used then. To manage master
+ keys in existing documents, use the "add-{kms,pgp,gcp-kms,azure-kv,hc-vault-transit}"
+ and "rm-{kms,pgp,gcp-kms,azure-kv,hc-vault-transit}" flags with --rotate
+ or the updatekeys command.
+
+ To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC.
+
+ To select a different editor than the default (vim), set EDITOR.
+
+ Note that flags must always be provided before the filename to operate on.
+ Otherwise, they will be ignored.
+
+ For more information, see the README at https://github.com/getsops/sops`
+ app.EnableBashCompletion = true
+ app.Commands = []cli.Command{
+ {
+ Name: "exec-env",
+ Usage: "execute a command with decrypted values inserted into the environment",
+ ArgsUsage: "[file to decrypt] [command to run]",
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "background",
+ Usage: "background the process and don't wait for it to complete (DEPRECATED)",
+ },
+ cli.BoolFlag{
+ Name: "pristine",
+ Usage: "insert only the decrypted values into the environment without forwarding existing environment variables",
+ },
+ cli.StringFlag{
+ Name: "user",
+ Usage: "the user to run the command as",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.NArg() != 2 {
+ return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric)
+ }
+
+ fileName := c.Args()[0]
+ command := c.Args()[1]
+
+ inputStore := inputStore(c, fileName)
+
+ svcs := keyservices(c)
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ opts := decryptOpts{
+ OutputStore: &dotenv.Store{},
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ }
+
+ if c.Bool("background") {
+ log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops")
+ }
+
+ tree, err := decryptTree(opts)
+ if err != nil {
+ return toExitError(err)
+ }
+
+ var env []string
+ for _, item := range tree.Branches[0] {
+ if dotenv.IsComplexValue(item.Value) {
+ return cli.NewExitError(fmt.Errorf("cannot use complex value in environment: %s", item.Value), codes.ErrorGeneric)
+ }
+ if _, ok := item.Key.(sops.Comment); ok {
+ continue
+ }
+ key, ok := item.Key.(string)
+ if !ok {
+ return cli.NewExitError(fmt.Errorf("cannot use non-string keys in environment, got %T", item.Key), codes.ErrorGeneric)
+ }
+ if strings.Contains(key, "=") {
+ return cli.NewExitError(fmt.Errorf("cannot use keys with '=' in environment: %s", key), codes.ErrorGeneric)
+ }
+ value, ok := item.Value.(string)
+ if !ok {
+ return cli.NewExitError(fmt.Errorf("cannot use non-string values in environment, got %T", item.Value), codes.ErrorGeneric)
+ }
+ env = append(env, fmt.Sprintf("%s=%s", key, value))
+ }
+
+ if err := exec.ExecWithEnv(exec.ExecOpts{
+ Command: command,
+ Plaintext: []byte{},
+ Background: c.Bool("background"),
+ Pristine: c.Bool("pristine"),
+ User: c.String("user"),
+ Env: env,
+ }); err != nil {
+ return toExitError(err)
+ }
+
+ return nil
+ },
+ },
+ {
+ Name: "exec-file",
+ Usage: "execute a command with the decrypted contents as a temporary file",
+ ArgsUsage: "[file to decrypt] [command to run]",
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "background",
+ Usage: "background the process and don't wait for it to complete (DEPRECATED)",
+ },
+ cli.BoolFlag{
+ Name: "no-fifo",
+ Usage: "use a regular file instead of a fifo to temporarily hold the decrypted contents",
+ },
+ cli.StringFlag{
+ Name: "user",
+ Usage: "the user to run the command as",
+ },
+ cli.StringFlag{
+ Name: "input-type",
+ Usage: "currently ini, 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 ini, json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
+ },
+ cli.StringFlag{
+ Name: "filename",
+ Usage: fmt.Sprintf("filename for the temporarily file (default: %s)", exec.FallbackFilename),
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.NArg() != 2 {
+ return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric)
+ }
+
+ fileName := c.Args()[0]
+ command := c.Args()[1]
+
+ inputStore := inputStore(c, fileName)
+ outputStore := outputStore(c, fileName)
+
+ svcs := keyservices(c)
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ opts := decryptOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ }
+
+ output, err := decrypt(opts)
+ if err != nil {
+ return toExitError(err)
+ }
+
+ if c.Bool("background") {
+ log.Warn("exec-file's --background option is deprecated and will be removed in a future version of sops")
+ }
+
+ if err := exec.ExecWithFile(exec.ExecOpts{
+ Command: command,
+ Plaintext: output,
+ Background: c.Bool("background"),
+ Fifo: !c.Bool("no-fifo"),
+ User: c.String("user"),
+ Filename: c.String("filename"),
+ }); err != nil {
+ return toExitError(err)
+ }
+
+ return nil
+ },
+ },
+ {
+ Name: "publish",
+ Usage: "Publish sops file or directory to a configured destination",
+ ArgsUsage: `file`,
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "yes, y",
+ Usage: `pre-approve all changes and run non-interactively`,
+ },
+ cli.BoolFlag{
+ Name: "omit-extensions",
+ Usage: "Omit file extensions in destination path when publishing sops file to configured destinations",
+ },
+ cli.BoolFlag{
+ Name: "recursive",
+ Usage: "If the source path is a directory, publish all its content recursively",
+ },
+ cli.BoolFlag{
+ Name: "verbose",
+ Usage: "Enable verbose logging output",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") || c.GlobalBool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ configPath, err := config.FindConfigFile(".")
+ if err != nil {
+ return common.NewExitError(err, codes.ErrorGeneric)
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ warnMoreThanOnePositionalArgument(c)
+ path := c.Args()[0]
+ info, err := os.Stat(path)
+ if err != nil {
+ return toExitError(err)
+ }
+ if info.IsDir() && !c.Bool("recursive") {
+ return fmt.Errorf("can't operate on a directory without --recursive flag.")
+ }
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ err = filepath.Walk(path, func(subPath string, info os.FileInfo, err error) error {
+ if err != nil {
+ return toExitError(err)
+ }
+ if !info.IsDir() {
+ err = publishcmd.Run(publishcmd.Opts{
+ ConfigPath: configPath,
+ InputPath: subPath,
+ Cipher: aes.NewCipher(),
+ KeyServices: keyservices(c),
+ DecryptionOrder: order,
+ InputStore: inputStore(c, subPath),
+ Interactive: !c.Bool("yes"),
+ OmitExtensions: c.Bool("omit-extensions"),
+ Recursive: c.Bool("recursive"),
+ })
+ if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
+ return cliErr
+ } else if err != nil {
+ return common.NewExitError(err, codes.ErrorGeneric)
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return toExitError(err)
+ }
+ return nil
+ },
+ },
+ {
+ Name: "keyservice",
+ Usage: "start a SOPS key service server",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "network, net",
+ Usage: "network to listen on, e.g. 'tcp' or 'unix'",
+ Value: "tcp",
+ },
+ cli.StringFlag{
+ Name: "address, addr",
+ Usage: "address to listen on, e.g. '127.0.0.1:5000' or '/tmp/sops.sock'",
+ Value: "127.0.0.1:5000",
+ },
+ cli.BoolFlag{
+ Name: "prompt",
+ Usage: "Prompt user to confirm every incoming request",
+ },
+ cli.BoolFlag{
+ Name: "verbose",
+ Usage: "Enable verbose logging output",
+ },
+ },
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") || c.GlobalBool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ err := keyservicecmd.Run(keyservicecmd.Opts{
+ Network: c.String("network"),
+ Address: c.String("address"),
+ Prompt: c.Bool("prompt"),
+ })
+ if err != nil {
+ log.Errorf("Error running keyservice: %s", err)
+ return err
+ }
+ return nil
+ },
+ },
+ {
+ Name: "filestatus",
+ Usage: "check the status of the file, returning encryption status",
+ ArgsUsage: `file`,
+ Flags: []cli.Flag{},
+ Action: func(c *cli.Context) error {
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+
+ fileName := c.Args()[0]
+ inputStore := inputStore(c, fileName)
+ opts := filestatuscmd.Opts{
+ InputStore: inputStore,
+ InputPath: fileName,
+ }
+
+ status, err := filestatuscmd.FileStatus(opts)
+ if err != nil {
+ return err
+ }
+
+ json, err := encodingjson.Marshal(status)
+ if err != nil {
+ return common.NewExitError(err, codes.ErrorGeneric)
+ }
+
+ fmt.Println(string(json))
+
+ return nil
+ },
+ },
+ {
+ Name: "groups",
+ Usage: "modify the groups on a SOPS file",
+ Subcommands: []cli.Command{
+ {
+ Name: "add",
+ Usage: "add a new group to a SOPS file",
+ Flags: append([]cli.Flag{
+ cli.StringFlag{
+ Name: "file, f",
+ Usage: "the file to add the group to",
+ },
+ cli.StringSliceFlag{
+ Name: "pgp",
+ Usage: "the PGP fingerprints the new group should contain. Can be specified more than once",
+ },
+ cli.StringSliceFlag{
+ Name: "kms",
+ Usage: "the KMS ARNs the new group should contain. Can be specified more than once",
+ },
+ cli.StringFlag{
+ Name: "aws-profile",
+ Usage: "The AWS profile to use for requests to AWS",
+ },
+ cli.StringSliceFlag{
+ Name: "gcp-kms",
+ Usage: "the GCP KMS Resource ID the new group should contain. Can be specified more than once",
+ },
+ cli.StringSliceFlag{
+ Name: "azure-kv",
+ Usage: "the Azure Key Vault key URL the new group should contain. Can be specified more than once",
+ },
+ cli.StringSliceFlag{
+ Name: "hc-vault-transit",
+ Usage: "the full vault path to the key used to encrypt/decrypt. Make you choose and configure a key with encryption/decryption enabled (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev'). Can be specified more than once",
+ },
+ cli.StringSliceFlag{
+ Name: "age",
+ Usage: "the age recipient the new group should contain. Can be specified more than once",
+ },
+ cli.BoolFlag{
+ Name: "in-place, i",
+ Usage: "write output back to the same file instead of stdout",
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ cli.StringFlag{
+ Name: "encryption-context",
+ Usage: "comma separated list of KMS encryption context key:value pairs",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ pgpFps := c.StringSlice("pgp")
+ kmsArns := c.StringSlice("kms")
+ gcpKmses := c.StringSlice("gcp-kms")
+ vaultURIs := c.StringSlice("hc-vault-transit")
+ azkvs := c.StringSlice("azure-kv")
+ ageRecipients := c.StringSlice("age")
+ if c.NArg() != 0 {
+ return common.NewExitError(fmt.Errorf("error: no positional arguments allowed"), codes.ErrorGeneric)
+ }
+ var group sops.KeyGroup
+ for _, fp := range pgpFps {
+ group = append(group, pgp.NewMasterKeyFromFingerprint(fp))
+ }
+ for _, arn := range kmsArns {
+ group = append(group, kms.NewMasterKeyFromArn(arn, kms.ParseKMSContext(c.String("encryption-context")), c.String("aws-profile")))
+ }
+ for _, kms := range gcpKmses {
+ group = append(group, gcpkms.NewMasterKeyFromResourceID(kms))
+ }
+ for _, uri := range vaultURIs {
+ k, err := hcvault.NewMasterKeyFromURI(uri)
+ if err != nil {
+ log.WithError(err).Error("Failed to add key")
+ continue
+ }
+ group = append(group, k)
+ }
+ for _, url := range azkvs {
+ k, err := azkv.NewMasterKeyFromURL(url)
+ if err != nil {
+ log.WithError(err).Error("Failed to add key")
+ continue
+ }
+ group = append(group, k)
+ }
+ for _, recipient := range ageRecipients {
+ keys, err := age.MasterKeysFromRecipients(recipient)
+ if err != nil {
+ log.WithError(err).Error("Failed to add key")
+ continue
+ }
+ for _, key := range keys {
+ group = append(group, key)
+ }
+ }
+ return groups.Add(groups.AddOpts{
+ InputPath: c.String("file"),
+ InPlace: c.Bool("in-place"),
+ InputStore: inputStore(c, c.String("file")),
+ OutputStore: outputStore(c, c.String("file")),
+ Group: group,
+ GroupThreshold: c.Int("shamir-secret-sharing-threshold"),
+ KeyServices: keyservices(c),
+ })
+ },
+ },
+ {
+ Name: "delete",
+ Usage: "delete a key group from a SOPS file",
+ Flags: append([]cli.Flag{
+ cli.StringFlag{
+ Name: "file, f",
+ Usage: "the file to add the group to",
+ },
+ cli.BoolFlag{
+ Name: "in-place, i",
+ Usage: "write output back to the same file instead of stdout",
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ }, keyserviceFlags...),
+ ArgsUsage: `[index]`,
+
+ Action: func(c *cli.Context) error {
+ if c.NArg() != 1 {
+ return common.NewExitError(fmt.Errorf("error: exactly one positional argument (index) required"), codes.ErrorGeneric)
+ }
+ group, err := strconv.ParseUint(c.Args().First(), 10, 32)
+ if err != nil {
+ return fmt.Errorf("failed to parse [index] argument: %s", err)
+ }
+
+ return groups.Delete(groups.DeleteOpts{
+ InputPath: c.String("file"),
+ InPlace: c.Bool("in-place"),
+ InputStore: inputStore(c, c.String("file")),
+ OutputStore: outputStore(c, c.String("file")),
+ Group: uint(group),
+ GroupThreshold: c.Int("shamir-secret-sharing-threshold"),
+ KeyServices: keyservices(c),
+ })
+ },
+ },
+ },
+ },
+ {
+ Name: "updatekeys",
+ Usage: "update the keys of SOPS files using the config file",
+ ArgsUsage: `file`,
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "yes, y",
+ Usage: `pre-approve all changes and run non-interactively`,
+ },
+ cli.StringFlag{
+ Name: "input-type",
+ Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ var err error
+ var configPath string
+ if c.GlobalString("config") != "" {
+ configPath = c.GlobalString("config")
+ } else {
+ configPath, err = config.FindConfigFile(".")
+ if err != nil {
+ return common.NewExitError(err, codes.ErrorGeneric)
+ }
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ failedCounter := 0
+ for _, path := range c.Args() {
+ err := updatekeys.UpdateKeys(updatekeys.Opts{
+ InputPath: path,
+ GroupQuorum: c.Int("shamir-secret-sharing-quorum"),
+ KeyServices: keyservices(c),
+ Interactive: !c.Bool("yes"),
+ ConfigPath: configPath,
+ InputType: c.String("input-type"),
+ })
+
+ if c.NArg() == 1 {
+ // a single argument was given, keep compatibility of the error
+ if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
+ return cliErr
+ } else if err != nil {
+ return common.NewExitError(err, codes.ErrorGeneric)
+ }
+ }
+
+ // multiple arguments given (patched functionality),
+ // finish updating of remaining files and fail afterwards
+ if err != nil {
+ failedCounter++
+ log.Error(err)
+ }
+ }
+ if failedCounter > 0 {
+ return common.NewExitError(fmt.Errorf("failed updating %d key(s)", failedCounter), codes.ErrorGeneric)
+ }
+ return nil
+ },
+ },
+ {
+ Name: "decrypt",
+ Usage: "decrypt a file, and output the results to stdout",
+ ArgsUsage: `file`,
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "in-place, i",
+ Usage: "write output back to the same file instead of stdout",
+ },
+ cli.StringFlag{
+ Name: "extract",
+ Usage: "extract a specific key or branch from the input document. Example: --extract '[\"somekey\"][0]'",
+ },
+ cli.StringFlag{
+ Name: "output",
+ Usage: "Save the output after decryption to the file specified",
+ },
+ 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",
+ },
+ 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",
+ },
+ cli.BoolFlag{
+ Name: "ignore-mac",
+ Usage: "ignore Message Authentication Code during decryption",
+ },
+ cli.StringFlag{
+ Name: "filename-override",
+ Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+ },
+ cli.StringFlag{
+ Name: "decryption-order",
+ Usage: "comma separated list of decryption key types",
+ EnvVar: "SOPS_DECRYPTION_ORDER",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ warnMoreThanOnePositionalArgument(c)
+ if c.Bool("in-place") && c.String("output") != "" {
+ return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+ }
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+ if _, err := os.Stat(fileName); os.IsNotExist(err) {
+ return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+ }
+ fileNameOverride := c.String("filename-override")
+ if fileNameOverride == "" {
+ fileNameOverride = fileName
+ }
+
+ inputStore := inputStore(c, fileNameOverride)
+ outputStore := outputStore(c, fileNameOverride)
+ svcs := keyservices(c)
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+
+ var extract []interface{}
+ extract, err = parseTreePath(c.String("extract"))
+ if err != nil {
+ return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
+ }
+ output, err := decrypt(decryptOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ Extract: extract,
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ })
+ if err != nil {
+ return toExitError(err)
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ if c.Bool("in-place") {
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ }
+
+ outputFile := os.Stdout
+ if c.String("output") != "" {
+ file, err := os.Create(c.String("output"))
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ outputFile = file
+ }
+ _, err = outputFile.Write(output)
+ return toExitError(err)
+ },
+ },
+ {
+ Name: "encrypt",
+ Usage: "encrypt a file, and output the results to stdout",
+ ArgsUsage: `file`,
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "in-place, i",
+ Usage: "write output back to the same file instead of stdout",
+ },
+ cli.StringFlag{
+ Name: "output",
+ Usage: "Save the output after decryption to the file specified",
+ },
+ cli.StringFlag{
+ Name: "kms, k",
+ Usage: "comma separated list of KMS ARNs",
+ EnvVar: "SOPS_KMS_ARN",
+ },
+ cli.StringFlag{
+ Name: "aws-profile",
+ Usage: "The AWS profile to use for requests to AWS",
+ },
+ cli.StringFlag{
+ Name: "gcp-kms",
+ Usage: "comma separated list of GCP KMS resource IDs",
+ EnvVar: "SOPS_GCP_KMS_IDS",
+ },
+ cli.StringFlag{
+ Name: "azure-kv",
+ Usage: "comma separated list of Azure Key Vault URLs",
+ EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
+ },
+ cli.StringFlag{
+ Name: "hc-vault-transit",
+ Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
+ EnvVar: "SOPS_VAULT_URIS",
+ },
+ cli.StringFlag{
+ Name: "pgp, p",
+ Usage: "comma separated list of PGP fingerprints",
+ EnvVar: "SOPS_PGP_FP",
+ },
+ cli.StringFlag{
+ Name: "age, a",
+ Usage: "comma separated list of age recipients",
+ EnvVar: "SOPS_AGE_RECIPIENTS",
+ },
+ 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",
+ },
+ 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",
+ },
+ cli.StringFlag{
+ Name: "unencrypted-suffix",
+ Usage: "override the unencrypted key suffix.",
+ },
+ cli.StringFlag{
+ 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 regex. When specified, only keys matching the regex will be left unencrypted.",
+ },
+ cli.StringFlag{
+ Name: "encrypted-regex",
+ Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
+ },
+ cli.StringFlag{
+ Name: "encryption-context",
+ Usage: "comma separated list of KMS encryption context key:value pairs",
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ cli.StringFlag{
+ Name: "filename-override",
+ Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ warnMoreThanOnePositionalArgument(c)
+ if c.Bool("in-place") && c.String("output") != "" {
+ return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+ }
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+ if _, err := os.Stat(fileName); os.IsNotExist(err) {
+ return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+ }
+ fileNameOverride := c.String("filename-override")
+ if fileNameOverride == "" {
+ fileNameOverride = fileName
+ }
+
+ inputStore := inputStore(c, fileNameOverride)
+ outputStore := outputStore(c, fileNameOverride)
+ svcs := keyservices(c)
+
+ encConfig, err := getEncryptConfig(c, fileNameOverride)
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err := encrypt(encryptOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ encryptConfig: encConfig,
+ })
+
+ if err != nil {
+ return toExitError(err)
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ if c.Bool("in-place") {
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ }
+
+ outputFile := os.Stdout
+ if c.String("output") != "" {
+ file, err := os.Create(c.String("output"))
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ outputFile = file
+ }
+ _, err = outputFile.Write(output)
+ return toExitError(err)
+ },
+ },
+ {
+ Name: "rotate",
+ Usage: "generate a new data encryption key and reencrypt all values with the new key",
+ ArgsUsage: `file`,
+ Flags: append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "in-place, i",
+ Usage: "write output back to the same file instead of stdout",
+ },
+ cli.StringFlag{
+ Name: "output",
+ Usage: "Save the output after decryption to the file specified",
+ },
+ 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",
+ },
+ 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",
+ },
+ cli.StringFlag{
+ Name: "encryption-context",
+ Usage: "comma separated list of KMS encryption context key:value pairs",
+ },
+ cli.StringFlag{
+ Name: "add-gcp-kms",
+ Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-gcp-kms",
+ Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-azure-kv",
+ Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-azure-kv",
+ Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-kms",
+ Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-kms",
+ Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-hc-vault-transit",
+ Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
+ },
+ cli.StringFlag{
+ Name: "rm-hc-vault-transit",
+ Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
+ },
+ cli.StringFlag{
+ Name: "add-age",
+ Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-age",
+ Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-pgp",
+ Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-pgp",
+ Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "filename-override",
+ Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+ },
+ cli.StringFlag{
+ Name: "decryption-order",
+ Usage: "comma separated list of decryption key types",
+ EnvVar: "SOPS_DECRYPTION_ORDER",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ warnMoreThanOnePositionalArgument(c)
+ if c.Bool("in-place") && c.String("output") != "" {
+ return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+ }
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+ if _, err := os.Stat(fileName); os.IsNotExist(err) {
+ if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
+ c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
+ return common.NewExitError("Error: cannot add or remove keys on non-existent files, use the `edit` subcommand instead.", codes.CannotChangeKeysFromNonExistentFile)
+ }
+ }
+ fileNameOverride := c.String("filename-override")
+ if fileNameOverride == "" {
+ fileNameOverride = fileName
+ }
+
+ inputStore := inputStore(c, fileNameOverride)
+ outputStore := outputStore(c, fileNameOverride)
+ svcs := keyservices(c)
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+
+ rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err := rotate(rotateOpts)
+ if err != nil {
+ return toExitError(err)
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ if c.Bool("in-place") {
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ }
+
+ outputFile := os.Stdout
+ if c.String("output") != "" {
+ file, err := os.Create(c.String("output"))
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ outputFile = file
+ }
+ _, err = outputFile.Write(output)
+ return toExitError(err)
+ },
+ },
+ {
+ Name: "edit",
+ Usage: "edit an encrypted file",
+ ArgsUsage: `file`,
+ Flags: append([]cli.Flag{
+ cli.StringFlag{
+ Name: "kms, k",
+ Usage: "comma separated list of KMS ARNs",
+ EnvVar: "SOPS_KMS_ARN",
+ },
+ cli.StringFlag{
+ Name: "aws-profile",
+ Usage: "The AWS profile to use for requests to AWS",
+ },
+ cli.StringFlag{
+ Name: "gcp-kms",
+ Usage: "comma separated list of GCP KMS resource IDs",
+ EnvVar: "SOPS_GCP_KMS_IDS",
+ },
+ cli.StringFlag{
+ Name: "azure-kv",
+ Usage: "comma separated list of Azure Key Vault URLs",
+ EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
+ },
+ cli.StringFlag{
+ Name: "hc-vault-transit",
+ Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
+ EnvVar: "SOPS_VAULT_URIS",
+ },
+ cli.StringFlag{
+ Name: "pgp, p",
+ Usage: "comma separated list of PGP fingerprints",
+ EnvVar: "SOPS_PGP_FP",
+ },
+ cli.StringFlag{
+ Name: "age, a",
+ Usage: "comma separated list of age recipients",
+ EnvVar: "SOPS_AGE_RECIPIENTS",
+ },
+ 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",
+ },
+ 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",
+ },
+ cli.StringFlag{
+ Name: "unencrypted-suffix",
+ Usage: "override the unencrypted key suffix.",
+ },
+ cli.StringFlag{
+ 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 regex. When specified, only keys matching the regex will be left unencrypted.",
+ },
+ cli.StringFlag{
+ Name: "encrypted-regex",
+ Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
+ },
+ cli.StringFlag{
+ Name: "encryption-context",
+ Usage: "comma separated list of KMS encryption context key:value pairs",
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ cli.BoolFlag{
+ Name: "show-master-keys, s",
+ Usage: "display master encryption keys in the file during editing",
+ },
+ cli.BoolFlag{
+ Name: "ignore-mac",
+ Usage: "ignore Message Authentication Code during decryption",
+ },
+ cli.StringFlag{
+ Name: "decryption-order",
+ Usage: "comma separated list of decryption key types",
+ EnvVar: "SOPS_DECRYPTION_ORDER",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ warnMoreThanOnePositionalArgument(c)
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+ if _, err := os.Stat(fileName); os.IsNotExist(err) {
+ return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+ }
+
+ inputStore := inputStore(c, fileName)
+ outputStore := outputStore(c, fileName)
+ svcs := keyservices(c)
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ var output []byte
+ _, statErr := os.Stat(fileName)
+ fileExists := statErr == nil
+ opts := editOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ ShowMasterKeys: c.Bool("show-master-keys"),
+ }
+ if fileExists {
+ output, err = edit(opts)
+ if err != nil {
+ return toExitError(err)
+ }
+ } else {
+ // File doesn't exist, edit the example file instead
+ encConfig, err := getEncryptConfig(c, fileName)
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err = editExample(editExampleOpts{
+ editOpts: opts,
+ encryptConfig: encConfig,
+ })
+ if err != nil {
+ return toExitError(err)
+ }
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ },
+ },
+ {
+ Name: "set",
+ Usage: `set a specific key or branch in the input document. value must be a json encoded string. eg. '/path/to/file ["somekey"][0] {"somevalue":true}'`,
+ ArgsUsage: `file index value`,
+ Flags: append([]cli.Flag{
+ 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",
+ },
+ 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",
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ cli.BoolFlag{
+ Name: "ignore-mac",
+ Usage: "ignore Message Authentication Code during decryption",
+ },
+ cli.StringFlag{
+ Name: "decryption-order",
+ Usage: "comma separated list of decryption key types",
+ EnvVar: "SOPS_DECRYPTION_ORDER",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() != 3 {
+ return common.NewExitError("Error: no file specified, or index and value are missing", codes.NoFileSpecified)
+ }
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+
+ inputStore := inputStore(c, fileName)
+ outputStore := outputStore(c, fileName)
+ svcs := keyservices(c)
+
+ path, err := parseTreePath(c.Args()[1])
+ if err != nil {
+ return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat)
+ }
+
+ value, err := jsonValueToTreeInsertableValue(c.Args()[2])
+ if err != nil {
+ return toExitError(err)
+ }
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err := set(setOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ Value: value,
+ TreePath: path,
+ })
+ if err != nil {
+ return toExitError(err)
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ },
+ },
+ {
+ Name: "unset",
+ Usage: `unset a specific key or branch in the input document.`,
+ ArgsUsage: `file index`,
+ Flags: append([]cli.Flag{
+ 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",
+ },
+ 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",
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ cli.BoolFlag{
+ Name: "ignore-mac",
+ Usage: "ignore Message Authentication Code during decryption",
+ },
+ cli.StringFlag{
+ Name: "decryption-order",
+ Usage: "comma separated list of decryption key types",
+ EnvVar: "SOPS_DECRYPTION_ORDER",
+ },
+ cli.BoolFlag{
+ Name: "idempotent",
+ Usage: "do nothing if the given index does not exist",
+ },
+ }, keyserviceFlags...),
+ Action: func(c *cli.Context) error {
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() != 2 {
+ return common.NewExitError("Error: no file specified, or index is missing", codes.NoFileSpecified)
+ }
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+
+ inputStore := inputStore(c, fileName)
+ outputStore := outputStore(c, fileName)
+ svcs := keyservices(c)
+
+ path, err := parseTreePath(c.Args()[1])
+ if err != nil {
+ return common.NewExitError("Invalid unset index format", codes.ErrorInvalidSetFormat)
+ }
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err := unset(unsetOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ TreePath: path,
+ })
+ if err != nil {
+ if _, ok := err.(*sops.SopsKeyNotFound); ok && c.Bool("idempotent") {
+ return nil
+ }
+ return toExitError(err)
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ },
+ },
+ }
+ app.Flags = append([]cli.Flag{
+ cli.BoolFlag{
+ Name: "decrypt, d",
+ Usage: "decrypt a file and output the result to stdout",
+ },
+ cli.BoolFlag{
+ Name: "encrypt, e",
+ Usage: "encrypt a file and output the result to stdout",
+ },
+ cli.BoolFlag{
+ Name: "rotate, r",
+ Usage: "generate a new data encryption key and reencrypt all values with the new key",
+ },
+ cli.BoolFlag{
+ Name: "disable-version-check",
+ Usage: "do not check whether the current version is latest during --version",
+ },
+ cli.StringFlag{
+ Name: "kms, k",
+ Usage: "comma separated list of KMS ARNs",
+ EnvVar: "SOPS_KMS_ARN",
+ },
+ cli.StringFlag{
+ Name: "aws-profile",
+ Usage: "The AWS profile to use for requests to AWS",
+ },
+ cli.StringFlag{
+ Name: "gcp-kms",
+ Usage: "comma separated list of GCP KMS resource IDs",
+ EnvVar: "SOPS_GCP_KMS_IDS",
+ },
+ cli.StringFlag{
+ Name: "azure-kv",
+ Usage: "comma separated list of Azure Key Vault URLs",
+ EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
+ },
+ cli.StringFlag{
+ Name: "hc-vault-transit",
+ Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
+ EnvVar: "SOPS_VAULT_URIS",
+ },
+ cli.StringFlag{
+ Name: "pgp, p",
+ Usage: "comma separated list of PGP fingerprints",
+ EnvVar: "SOPS_PGP_FP",
+ },
+ cli.StringFlag{
+ Name: "age, a",
+ Usage: "comma separated list of age recipients",
+ EnvVar: "SOPS_AGE_RECIPIENTS",
+ },
+ cli.BoolFlag{
+ Name: "in-place, i",
+ Usage: "write output back to the same file instead of stdout",
+ },
+ cli.StringFlag{
+ Name: "extract",
+ Usage: "extract a specific key or branch from the input document. Decrypt mode only. Example: --extract '[\"somekey\"][0]'",
+ },
+ 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",
+ },
+ 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",
+ },
+ cli.BoolFlag{
+ Name: "show-master-keys, s",
+ Usage: "display master encryption keys in the file during editing",
+ },
+ cli.StringFlag{
+ Name: "add-gcp-kms",
+ Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-gcp-kms",
+ Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-azure-kv",
+ Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-azure-kv",
+ Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-kms",
+ Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-kms",
+ Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-hc-vault-transit",
+ Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
+ },
+ cli.StringFlag{
+ Name: "rm-hc-vault-transit",
+ Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
+ },
+ cli.StringFlag{
+ Name: "add-age",
+ Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-age",
+ Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "add-pgp",
+ Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file",
+ },
+ cli.StringFlag{
+ Name: "rm-pgp",
+ Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file",
+ },
+ cli.BoolFlag{
+ Name: "ignore-mac",
+ Usage: "ignore Message Authentication Code during decryption",
+ },
+ cli.BoolFlag{
+ Name: "mac-only-encrypted",
+ Usage: "compute MAC only over values which end up encrypted",
+ },
+ cli.StringFlag{
+ Name: "unencrypted-suffix",
+ Usage: "override the unencrypted key suffix.",
+ },
+ cli.StringFlag{
+ 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 regex. When specified, only keys matching the regex will be left unencrypted.",
+ },
+ cli.StringFlag{
+ Name: "encrypted-regex",
+ Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
+ },
+ cli.StringFlag{
+ Name: "unencrypted-comment-regex",
+ Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.",
+ },
+ cli.StringFlag{
+ Name: "encrypted-comment-regex",
+ Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.",
+ },
+ cli.StringFlag{
+ Name: "config",
+ Usage: "path to sops' config file. If set, sops will not search for the config file recursively.",
+ },
+ cli.StringFlag{
+ Name: "encryption-context",
+ Usage: "comma separated list of KMS encryption context key:value pairs",
+ },
+ cli.StringFlag{
+ Name: "set",
+ Usage: `set a specific key or branch in the input document. value must be a json encoded string. (edit mode only). eg. --set '["somekey"][0] {"somevalue":true}'`,
+ },
+ cli.IntFlag{
+ Name: "shamir-secret-sharing-threshold",
+ Usage: "the number of master keys required to retrieve the data key with shamir",
+ },
+ cli.IntFlag{
+ Name: "indent",
+ Usage: "the number of spaces to indent YAML or JSON encoded file",
+ },
+ cli.BoolFlag{
+ Name: "verbose",
+ Usage: "Enable verbose logging output",
+ },
+ cli.StringFlag{
+ Name: "output",
+ Usage: "Save the output after encryption or decryption to the file specified",
+ },
+ cli.StringFlag{
+ Name: "filename-override",
+ Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
+ },
+ cli.StringFlag{
+ Name: "decryption-order",
+ Usage: "comma separated list of decryption key types",
+ EnvVar: "SOPS_DECRYPTION_ORDER",
+ },
+ }, keyserviceFlags...)
+
+ app.Action = func(c *cli.Context) error {
+ isDecryptMode := c.Bool("decrypt")
+ isEncryptMode := c.Bool("encrypt")
+ isRotateMode := c.Bool("rotate")
+ isSetMode := c.String("set") != ""
+ isEditMode := !isEncryptMode && !isDecryptMode && !isRotateMode && !isSetMode
+
+ if c.Bool("verbose") {
+ logging.SetLevel(logrus.DebugLevel)
+ }
+ if c.NArg() < 1 {
+ return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
+ }
+ warnMoreThanOnePositionalArgument(c)
+ if c.Bool("in-place") && c.String("output") != "" {
+ return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
+ }
+ fileName, err := filepath.Abs(c.Args()[0])
+ if err != nil {
+ return toExitError(err)
+ }
+ if _, err := os.Stat(fileName); os.IsNotExist(err) {
+ if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
+ c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
+ return common.NewExitError("Error: cannot add or remove keys on non-existent files, use `--kms` and `--pgp` instead.", codes.CannotChangeKeysFromNonExistentFile)
+ }
+ if isEncryptMode || isDecryptMode || isRotateMode {
+ return common.NewExitError("Error: cannot operate on non-existent file", codes.NoFileSpecified)
+ }
+ }
+ fileNameOverride := c.String("filename-override")
+ if fileNameOverride == "" {
+ fileNameOverride = fileName
+ }
+
+ commandCount := 0
+ if isDecryptMode {
+ commandCount++
+ }
+ if isEncryptMode {
+ commandCount++
+ }
+ if isRotateMode {
+ commandCount++
+ }
+ if isSetMode {
+ commandCount++
+ }
+ if commandCount > 1 {
+ log.Warn("More than one command (--encrypt, --decrypt, --rotate, --set) has been specified. Only the changes made by the last one will be visible. Note that this behavior is deprecated and will cause an error eventually.")
+ }
+
+ // Load configuration here for backwards compatibility (error out in case of bad config files),
+ // but only when not just decrypting (https://github.com/getsops/sops/issues/868)
+ needsCreationRule := isEncryptMode || isRotateMode || isSetMode || isEditMode
+ if needsCreationRule {
+ _, err = loadConfig(c, fileNameOverride, nil)
+ if err != nil {
+ return toExitError(err)
+ }
+ }
+
+ inputStore := inputStore(c, fileNameOverride)
+ outputStore := outputStore(c, fileNameOverride)
+ svcs := keyservices(c)
+
+ order, err := decryptionOrder(c.String("decryption-order"))
+ if err != nil {
+ return toExitError(err)
+ }
+ var output []byte
+ if isEncryptMode {
+ encConfig, err := getEncryptConfig(c, fileNameOverride)
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err = encrypt(encryptOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ encryptConfig: encConfig,
+ })
+ // While this check is also done below, the `err` in this scope shadows
+ // the `err` in the outer scope. **Only** do this in case --decrypt,
+ // --rotate-, and --set are not specified, though, to keep old behavior.
+ if err != nil && !isDecryptMode && !isRotateMode && !isSetMode {
+ return toExitError(err)
+ }
+ }
+
+ if isDecryptMode {
+ var extract []interface{}
+ extract, err = parseTreePath(c.String("extract"))
+ if err != nil {
+ return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
+ }
+ output, err = decrypt(decryptOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ Extract: extract,
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ })
+ }
+ if isRotateMode {
+ rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
+ if err != nil {
+ return toExitError(err)
+ }
+
+ output, err = rotate(rotateOpts)
+ // While this check is also done below, the `err` in this scope shadows
+ // the `err` in the outer scope
+ if err != nil {
+ return toExitError(err)
+ }
+ }
+
+ if isSetMode {
+ var path []interface{}
+ var value interface{}
+ path, value, err = extractSetArguments(c.String("set"))
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err = set(setOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ Value: value,
+ TreePath: path,
+ })
+ }
+
+ if isEditMode {
+ _, statErr := os.Stat(fileName)
+ fileExists := statErr == nil
+ opts := editOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: order,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ ShowMasterKeys: c.Bool("show-master-keys"),
+ }
+ if fileExists {
+ output, err = edit(opts)
+ } else {
+ // File doesn't exist, edit the example file instead
+ encConfig, err := getEncryptConfig(c, fileNameOverride)
+ if err != nil {
+ return toExitError(err)
+ }
+ output, err = editExample(editExampleOpts{
+ editOpts: opts,
+ encryptConfig: encConfig,
+ })
+ // While this check is also done below, the `err` in this scope shadows
+ // the `err` in the outer scope
+ if err != nil {
+ return toExitError(err)
+ }
+ }
+ }
+
+ if err != nil {
+ return toExitError(err)
+ }
+
+ // We open the file *after* the operations on the tree have been
+ // executed to avoid truncating it when there's errors
+ if c.Bool("in-place") || isEditMode || isSetMode {
+ file, err := os.Create(fileName)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ _, err = file.Write(output)
+ if err != nil {
+ return toExitError(err)
+ }
+ log.Info("File written successfully")
+ return nil
+ }
+
+ outputFile := os.Stdout
+ if c.String("output") != "" {
+ file, err := os.Create(c.String("output"))
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
+ }
+ defer file.Close()
+ outputFile = file
+ }
+ _, err = outputFile.Write(output)
+ return toExitError(err)
+ }
+ err := app.Run(os.Args)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) {
+ unencryptedSuffix := c.String("unencrypted-suffix")
+ encryptedSuffix := c.String("encrypted-suffix")
+ encryptedRegex := c.String("encrypted-regex")
+ unencryptedRegex := c.String("unencrypted-regex")
+ encryptedCommentRegex := c.String("encrypted-comment-regex")
+ unencryptedCommentRegex := c.String("unencrypted-comment-regex")
+ macOnlyEncrypted := c.Bool("mac-only-encrypted")
+ conf, err := loadConfig(c, fileName, nil)
+ if err != nil {
+ return encryptConfig{}, toExitError(err)
+ }
+ if conf != nil {
+ // command line options have precedence
+ if unencryptedSuffix == "" {
+ unencryptedSuffix = conf.UnencryptedSuffix
+ }
+ if encryptedSuffix == "" {
+ encryptedSuffix = conf.EncryptedSuffix
+ }
+ if encryptedRegex == "" {
+ encryptedRegex = conf.EncryptedRegex
+ }
+ if unencryptedRegex == "" {
+ unencryptedRegex = conf.UnencryptedRegex
+ }
+ if encryptedCommentRegex == "" {
+ encryptedCommentRegex = conf.EncryptedCommentRegex
+ }
+ if unencryptedCommentRegex == "" {
+ unencryptedCommentRegex = conf.UnencryptedCommentRegex
+ }
+ if !macOnlyEncrypted {
+ macOnlyEncrypted = conf.MACOnlyEncrypted
+ }
+ }
+
+ cryptRuleCount := 0
+ if unencryptedSuffix != "" {
+ cryptRuleCount++
+ }
+ if encryptedSuffix != "" {
+ cryptRuleCount++
+ }
+ if encryptedRegex != "" {
+ cryptRuleCount++
+ }
+ if unencryptedRegex != "" {
+ cryptRuleCount++
+ }
+ if encryptedCommentRegex != "" {
+ cryptRuleCount++
+ }
+ if unencryptedCommentRegex != "" {
+ cryptRuleCount++
+ }
+
+ if cryptRuleCount > 1 {
+ return encryptConfig{}, common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file", codes.ErrorConflictingParameters)
+ }
+
+ // only supply the default UnencryptedSuffix when EncryptedSuffix, EncryptedRegex, and others are not provided
+ if cryptRuleCount == 0 {
+ unencryptedSuffix = sops.DefaultUnencryptedSuffix
+ }
+
+ var groups []sops.KeyGroup
+ groups, err = keyGroups(c, fileName)
+ if err != nil {
+ return encryptConfig{}, err
+ }
+
+ var threshold int
+ threshold, err = shamirThreshold(c, fileName)
+ if err != nil {
+ return encryptConfig{}, err
+ }
+
+ return encryptConfig{
+ UnencryptedSuffix: unencryptedSuffix,
+ EncryptedSuffix: encryptedSuffix,
+ UnencryptedRegex: unencryptedRegex,
+ EncryptedRegex: encryptedRegex,
+ UnencryptedCommentRegex: unencryptedCommentRegex,
+ EncryptedCommentRegex: encryptedCommentRegex,
+ MACOnlyEncrypted: macOnlyEncrypted,
+ KeyGroups: groups,
+ GroupThreshold: threshold,
+ }, nil
+}
+
+func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) {
+ var masterKeys []keys.MasterKey
+ for _, k := range kms.MasterKeysFromArnString(c.String(kmsOptionName), kmsEncryptionContext, c.String("aws-profile")) {
+ masterKeys = append(masterKeys, k)
+ }
+ for _, k := range pgp.MasterKeysFromFingerprintString(c.String(pgpOptionName)) {
+ masterKeys = append(masterKeys, k)
+ }
+ for _, k := range gcpkms.MasterKeysFromResourceIDString(c.String(gcpKmsOptionName)) {
+ masterKeys = append(masterKeys, k)
+ }
+ azureKeys, err := azkv.MasterKeysFromURLs(c.String(azureKvOptionName))
+ if err != nil {
+ return nil, err
+ }
+ for _, k := range azureKeys {
+ masterKeys = append(masterKeys, k)
+ }
+ hcVaultKeys, err := hcvault.NewMasterKeysFromURIs(c.String(hcVaultTransitOptionName))
+ if err != nil {
+ return nil, err
+ }
+ for _, k := range hcVaultKeys {
+ masterKeys = append(masterKeys, k)
+ }
+ ageKeys, err := age.MasterKeysFromRecipients(c.String(ageOptionName))
+ if err != nil {
+ return nil, err
+ }
+ for _, k := range ageKeys {
+ masterKeys = append(masterKeys, k)
+ }
+ return masterKeys, nil
+}
+
+func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, outputStore common.Store, svcs []keyservice.KeyServiceClient, decryptionOrder []string) (rotateOpts, error) {
+ kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context"))
+ addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-azure-kv", "add-hc-vault-transit", "add-age")
+ if err != nil {
+ return rotateOpts{}, err
+ }
+ rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age")
+ if err != nil {
+ return rotateOpts{}, err
+ }
+ return rotateOpts{
+ OutputStore: outputStore,
+ InputStore: inputStore,
+ InputPath: fileName,
+ Cipher: aes.NewCipher(),
+ KeyServices: svcs,
+ DecryptionOrder: decryptionOrder,
+ IgnoreMAC: c.Bool("ignore-mac"),
+ AddMasterKeys: addMasterKeys,
+ RemoveMasterKeys: rmMasterKeys,
+ }, nil
+}
+
+func toExitError(err error) error {
+ if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
+ return cliErr
+ } else if execErr, ok := err.(*osExec.ExitError); ok && execErr != nil {
+ return cli.NewExitError(err, execErr.ExitCode())
+ } else if err != nil {
+ return cli.NewExitError(err, codes.ErrorGeneric)
+ }
+ return nil
+}
+
+func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) {
+ if c.Bool("enable-local-keyservice") {
+ svcs = append(svcs, keyservice.NewLocalClient())
+ }
+ uris := c.StringSlice("keyservice")
+ for _, uri := range uris {
+ url, err := url.Parse(uri)
+ if err != nil {
+ log.WithField("uri", uri).
+ Warnf("Error parsing URI for keyservice, skipping")
+ continue
+ }
+ addr := url.Host
+ if url.Scheme == "unix" {
+ addr = url.Path
+ }
+ opts := []grpc.DialOption{
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithContextDialer(
+ func(ctx context.Context, addr string) (net.Conn, error) {
+ return (&net.Dialer{}).DialContext(ctx, url.Scheme, addr)
+ },
+ ),
+ }
+ log.WithField(
+ "address",
+ fmt.Sprintf("%s://%s", url.Scheme, addr),
+ ).Infof("Connecting to key service")
+ conn, err := grpc.Dial(addr, opts...)
+ if err != nil {
+ log.Fatalf("failed to listen: %v", err)
+ }
+ svcs = append(svcs, keyservice.NewKeyServiceClient(conn))
+ }
+ return
+}
+
+func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, error) {
+ var configPath string
+ if context.String("config") != "" {
+ configPath = context.String("config")
+ } else {
+ // Ignore config not found errors returned from FindConfigFile since the config file is not mandatory
+ foundPath, err := config.FindConfigFile(".")
+ if err != nil {
+ return config.NewStoresConfig(), nil
+ }
+ configPath = foundPath
+ }
+ return config.LoadStoresConfig(configPath)
+}
+
+func inputStore(context *cli.Context, path string) common.Store {
+ storesConf, _ := loadStoresConfig(context, path)
+ return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("input-type"))
+}
+
+func outputStore(context *cli.Context, path string) common.Store {
+ storesConf, _ := loadStoresConfig(context, path)
+ if context.IsSet("indent") {
+ indent := context.Int("indent")
+ storesConf.YAML.Indent = indent
+ storesConf.JSON.Indent = indent
+ storesConf.JSONBinary.Indent = indent
+ }
+
+ return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("output-type"))
+}
+
+func parseTreePath(arg string) ([]interface{}, error) {
+ var path []interface{}
+ components := strings.Split(arg, "[")
+ for _, component := range components {
+ if component == "" {
+ continue
+ }
+ if component[len(component)-1] != ']' {
+ return nil, fmt.Errorf("component %s doesn't end with ]", component)
+ }
+ component = component[:len(component)-1]
+ if component[0] == byte('"') || component[0] == byte('\'') {
+ // The component is a string
+ component = component[1 : len(component)-1]
+ path = append(path, component)
+ } else {
+ // The component must be a number
+ i, err := strconv.Atoi(component)
+ if err != nil {
+ return nil, err
+ }
+ path = append(path, i)
+ }
+ }
+ return path, nil
+}
+
+func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) {
+ var kmsKeys []keys.MasterKey
+ var pgpKeys []keys.MasterKey
+ var cloudKmsKeys []keys.MasterKey
+ var azkvKeys []keys.MasterKey
+ var hcVaultMkKeys []keys.MasterKey
+ var ageMasterKeys []keys.MasterKey
+ kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context"))
+ if c.String("encryption-context") != "" && kmsEncryptionContext == nil {
+ return nil, common.NewExitError("Invalid KMS encryption context format", codes.ErrorInvalidKMSEncryptionContextFormat)
+ }
+ if c.String("kms") != "" {
+ for _, k := range kms.MasterKeysFromArnString(c.String("kms"), kmsEncryptionContext, c.String("aws-profile")) {
+ kmsKeys = append(kmsKeys, k)
+ }
+ }
+ if c.String("gcp-kms") != "" {
+ for _, k := range gcpkms.MasterKeysFromResourceIDString(c.String("gcp-kms")) {
+ cloudKmsKeys = append(cloudKmsKeys, k)
+ }
+ }
+ if c.String("azure-kv") != "" {
+ azureKeys, err := azkv.MasterKeysFromURLs(c.String("azure-kv"))
+ if err != nil {
+ return nil, err
+ }
+ for _, k := range azureKeys {
+ azkvKeys = append(azkvKeys, k)
+ }
+ }
+ if c.String("hc-vault-transit") != "" {
+ hcVaultKeys, err := hcvault.NewMasterKeysFromURIs(c.String("hc-vault-transit"))
+ if err != nil {
+ return nil, err
+ }
+ for _, k := range hcVaultKeys {
+ hcVaultMkKeys = append(hcVaultMkKeys, k)
+ }
+ }
+ if c.String("pgp") != "" {
+ for _, k := range pgp.MasterKeysFromFingerprintString(c.String("pgp")) {
+ pgpKeys = append(pgpKeys, k)
+ }
+ }
+ if c.String("age") != "" {
+ ageKeys, err := age.MasterKeysFromRecipients(c.String("age"))
+ if err != nil {
+ return nil, err
+ }
+ for _, k := range ageKeys {
+ ageMasterKeys = append(ageMasterKeys, k)
+ }
+ }
+ if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" {
+ conf, err := loadConfig(c, file, kmsEncryptionContext)
+ // config file might just not be supplied, without any error
+ if conf == nil {
+ errMsg := "config file not found, or has no creation rules, and no keys provided through command line options"
+ if err != nil {
+ errMsg = fmt.Sprintf("%s: %s", errMsg, err)
+ }
+ return nil, fmt.Errorf(errMsg)
+ }
+ return conf.KeyGroups, err
+ }
+ var group sops.KeyGroup
+ group = append(group, kmsKeys...)
+ group = append(group, cloudKmsKeys...)
+ group = append(group, azkvKeys...)
+ group = append(group, pgpKeys...)
+ group = append(group, hcVaultMkKeys...)
+ group = append(group, ageMasterKeys...)
+ log.Debugf("Master keys available: %+v", group)
+ return []sops.KeyGroup{group}, nil
+}
+
+// loadConfig will look for an existing config file, either provided through the command line, or using config.FindConfigFile.
+// Since a config file is not required, this function does not error when one is not found, and instead returns a nil config pointer
+func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) {
+ var err error
+ var configPath string
+ if c.String("config") != "" {
+ configPath = c.String("config")
+ } else {
+ // Ignore config not found errors returned from FindConfigFile since the config file is not mandatory
+ configPath, err = config.FindConfigFile(".")
+ if err != nil {
+ // If we can't find a config file, but we were not explicitly requested to, assume it does not exist
+ return nil, nil
+ }
+ }
+ conf, err := config.LoadCreationRuleForFile(configPath, file, kmsEncryptionContext)
+ if err != nil {
+ return nil, err
+ }
+ return conf, nil
+}
+
+func shamirThreshold(c *cli.Context, file string) (int, error) {
+ if c.Int("shamir-secret-sharing-threshold") != 0 {
+ return c.Int("shamir-secret-sharing-threshold"), nil
+ }
+ conf, err := loadConfig(c, file, nil)
+ if conf == nil {
+ // This takes care of the following two case:
+ // 1. No config was provided, or contains no creation rules. Err will be nil and ShamirThreshold will be the default value of 0.
+ // 2. We did find a config file, but failed to load it. In that case the calling function will print the error and exit.
+ return 0, err
+ }
+ return conf.ShamirThreshold, nil
+}
+
+func jsonValueToTreeInsertableValue(jsonValue string) (interface{}, error) {
+ var valueToInsert interface{}
+ err := encodingjson.Unmarshal([]byte(jsonValue), &valueToInsert)
+ if err != nil {
+ return nil, common.NewExitError("Value for --set is not valid JSON", codes.ErrorInvalidSetFormat)
+ }
+ // Check if decoding it as json we find a single value
+ // and not a map or slice, in which case we can't marshal
+ // it to a sops.TreeBranch
+ kind := reflect.ValueOf(valueToInsert).Kind()
+ if kind == reflect.Map || kind == reflect.Slice {
+ var err error
+ valueToInsert, err = (&json.Store{}).LoadPlainFile([]byte(jsonValue))
+ if err != nil {
+ return nil, common.NewExitError("Invalid --set value format", codes.ErrorInvalidSetFormat)
+ }
+ }
+ // Fix for #461
+ // Attempt conversion to TreeBranches to handle yaml multidoc. If conversion fails it's
+ // most likely a string value, so just return it as-is.
+ values, ok := valueToInsert.(sops.TreeBranches)
+ if !ok {
+ return valueToInsert, nil
+ }
+ return values[0], nil
+}
+
+func extractSetArguments(set string) (path []interface{}, valueToInsert interface{}, err error) {
+ // Set is a string with the format "python-dict-index json-value"
+ // Since python-dict-index has to end with ], we split at "] " to get the two parts
+ pathValuePair := strings.SplitAfterN(set, "] ", 2)
+ if len(pathValuePair) < 2 {
+ return nil, nil, common.NewExitError("Invalid --set format", codes.ErrorInvalidSetFormat)
+ }
+ fullPath := strings.TrimRight(pathValuePair[0], " ")
+ jsonValue := pathValuePair[1]
+ valueToInsert, err = jsonValueToTreeInsertableValue(jsonValue)
+ if err != nil {
+ // All errors returned by jsonValueToTreeInsertableValue are created by common.NewExitError(),
+ // so we can simply pass them on
+ return nil, nil, err
+ }
+
+ path, err = parseTreePath(fullPath)
+ if err != nil {
+ return nil, nil, common.NewExitError("Invalid --set format", codes.ErrorInvalidSetFormat)
+ }
+ return path, valueToInsert, nil
+}
+
+func decryptionOrder(decryptionOrder string) ([]string, error) {
+ if decryptionOrder == "" {
+ return sops.DefaultDecryptionOrder, nil
+ }
+ orderList := strings.Split(decryptionOrder, ",")
+ unique := make(map[string]struct{})
+ for _, v := range orderList {
+ if _, ok := unique[v]; ok {
+ return nil, common.NewExitError(fmt.Sprintf("Duplicate decryption key type: %s", v), codes.DuplicateDecryptionKeyType)
+ }
+ unique[v] = struct{}{}
+ }
+ return orderList, nil
+}
diff --git a/cmd/cmd/sops/rotate.go b/cmd/cmd/sops/rotate.go
new file mode 100644
index 000000000..072b558e5
--- /dev/null
+++ b/cmd/cmd/sops/rotate.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/audit"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keys"
+ "github.com/getsops/sops/v3/keyservice"
+)
+
+type rotateOpts struct {
+ Cipher sops.Cipher
+ InputStore sops.Store
+ OutputStore sops.Store
+ InputPath string
+ IgnoreMAC bool
+ AddMasterKeys []keys.MasterKey
+ RemoveMasterKeys []keys.MasterKey
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+func rotate(opts rotateOpts) ([]byte, error) {
+ tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
+ Cipher: opts.Cipher,
+ InputStore: opts.InputStore,
+ InputPath: opts.InputPath,
+ IgnoreMAC: opts.IgnoreMAC,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ audit.SubmitEvent(audit.RotateEvent{
+ File: tree.FilePath,
+ })
+
+ _, err = common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Add new master keys
+ for _, key := range opts.AddMasterKeys {
+ tree.Metadata.KeyGroups[0] = append(tree.Metadata.KeyGroups[0], key)
+ }
+ // Remove master keys
+ for _, rmKey := range opts.RemoveMasterKeys {
+ for i := range tree.Metadata.KeyGroups {
+ for j, groupKey := range tree.Metadata.KeyGroups[i] {
+ if rmKey.ToString() == groupKey.ToString() {
+ tree.Metadata.KeyGroups[i] = append(tree.Metadata.KeyGroups[i][:j], tree.Metadata.KeyGroups[i][j+1:]...)
+ }
+ }
+ }
+ }
+
+ // Create a new data key
+ dataKey, errs := tree.GenerateDataKeyWithKeyServices(opts.KeyServices)
+ if len(errs) > 0 {
+ err = fmt.Errorf("Could not generate data key: %s", errs)
+ return nil, err
+ }
+
+ // Reencrypt the file with the new key
+ err = common.EncryptTree(common.EncryptTreeOpts{
+ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ return encryptedFile, nil
+}
diff --git a/cmd/cmd/sops/set.go b/cmd/cmd/sops/set.go
new file mode 100644
index 000000000..a6e8ed357
--- /dev/null
+++ b/cmd/cmd/sops/set.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+)
+
+type setOpts struct {
+ Cipher sops.Cipher
+ InputStore sops.Store
+ OutputStore sops.Store
+ InputPath string
+ IgnoreMAC bool
+ TreePath []interface{}
+ Value interface{}
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+func set(opts setOpts) ([]byte, error) {
+ // Load the file
+ // TODO: Issue #173: if the file does not exist, create it with the contents passed in as opts.Value
+ tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
+ Cipher: opts.Cipher,
+ InputStore: opts.InputStore,
+ InputPath: opts.InputPath,
+ IgnoreMAC: opts.IgnoreMAC,
+ KeyServices: opts.KeyServices,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Decrypt the file
+ dataKey, err := common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the value
+ tree.Branches[0] = tree.Branches[0].Set(opts.TreePath, opts.Value)
+
+ err = common.EncryptTree(common.EncryptTreeOpts{
+ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ return encryptedFile, err
+}
diff --git a/cmd/cmd/sops/subcommand/exec/exec.go b/cmd/cmd/sops/subcommand/exec/exec.go
new file mode 100644
index 000000000..be74a31a4
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/exec/exec.go
@@ -0,0 +1,149 @@
+package exec
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/getsops/sops/v3/logging"
+
+ "github.com/sirupsen/logrus"
+)
+
+const (
+ FallbackFilename = "tmp-file"
+)
+
+var log *logrus.Logger
+
+func init() {
+ log = logging.NewLogger("EXEC")
+}
+
+type ExecOpts struct {
+ Command string
+ Plaintext []byte
+ Background bool
+ Pristine bool
+ Fifo bool
+ User string
+ Filename string
+ Env []string
+}
+
+func GetFile(dir, filename string) *os.File {
+ // If no filename is provided, create a random one based on FallbackFilename
+ if filename == "" {
+ handle, err := os.CreateTemp(dir, FallbackFilename)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return handle
+ }
+ // If a filename is provided, use that one
+ handle, err := os.Create(filepath.Join(dir, filename))
+ if err != nil {
+ log.Fatal(err)
+ }
+ // read+write for owner only
+ if err = handle.Chmod(0600); err != nil {
+ log.Fatal(err)
+ }
+ return handle
+}
+
+func ExecWithFile(opts ExecOpts) error {
+ if opts.User != "" {
+ SwitchUser(opts.User)
+ }
+
+ if runtime.GOOS == "windows" && opts.Fifo {
+ log.Warn("no fifos on windows, use --no-fifo next time")
+ opts.Fifo = false
+ }
+
+ dir, err := os.MkdirTemp("", ".sops")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ var filename string
+ if opts.Fifo {
+ // fifo handling needs to be async, even opening to write
+ // will block if there is no reader present
+ filename = opts.Filename
+ if filename == "" {
+ filename = FallbackFilename
+ }
+ filename = GetPipe(dir, filename)
+ go WritePipe(filename, opts.Plaintext)
+ } else {
+ // GetFile handles opts.Filename == "" specially, that's why we have
+ // to pass in opts.Filename without handling the fallback here
+ handle := GetFile(dir, opts.Filename)
+ handle.Write(opts.Plaintext)
+ handle.Close()
+ filename = handle.Name()
+ }
+
+ var env []string
+ if !opts.Pristine {
+ env = os.Environ()
+ }
+ env = append(env, opts.Env...)
+
+ placeholdered := strings.Replace(opts.Command, "{}", filename, -1)
+ cmd := BuildCommand(placeholdered)
+ cmd.Env = env
+
+ if opts.Background {
+ return cmd.Start()
+ }
+
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ return cmd.Run()
+}
+
+func ExecWithEnv(opts ExecOpts) error {
+ if opts.User != "" {
+ SwitchUser(opts.User)
+ }
+
+ var env []string
+
+ if !opts.Pristine {
+ env = os.Environ()
+ }
+
+ lines := bytes.Split(opts.Plaintext, []byte("\n"))
+ for _, line := range lines {
+ if len(line) == 0 {
+ continue
+ }
+ if line[0] == '#' {
+ continue
+ }
+ env = append(env, string(line))
+ }
+
+ env = append(env, opts.Env...)
+
+ cmd := BuildCommand(opts.Command)
+ cmd.Env = env
+
+ if opts.Background {
+ return cmd.Start()
+ }
+
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ return cmd.Run()
+}
diff --git a/cmd/cmd/sops/subcommand/exec/exec_unix.go b/cmd/cmd/sops/subcommand/exec/exec_unix.go
new file mode 100644
index 000000000..cc831e798
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/exec/exec_unix.go
@@ -0,0 +1,67 @@
+// +build !windows
+
+package exec
+
+import (
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "syscall"
+)
+
+func BuildCommand(command string) *exec.Cmd {
+ return exec.Command("/bin/sh", "-c", command)
+}
+
+func WritePipe(pipe string, contents []byte) {
+ handle, err := os.OpenFile(pipe, os.O_WRONLY, 0600)
+
+ if err != nil {
+ os.Remove(pipe)
+ log.Fatal(err)
+ }
+
+ handle.Write(contents)
+ handle.Close()
+}
+
+func GetPipe(dir, filename string) string {
+ tmpfn := filepath.Join(dir, filename)
+ err := syscall.Mkfifo(tmpfn, 0600)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return tmpfn
+}
+
+func SwitchUser(username string) {
+ user, err := user.Lookup(username)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ uid, _ := strconv.Atoi(user.Uid)
+
+ err = syscall.Setgid(uid)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = syscall.Setuid(uid)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = syscall.Setreuid(uid, uid)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = syscall.Setregid(uid, uid)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/cmd/cmd/sops/subcommand/exec/exec_windows.go b/cmd/cmd/sops/subcommand/exec/exec_windows.go
new file mode 100644
index 000000000..7e0f21d74
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/exec/exec_windows.go
@@ -0,0 +1,22 @@
+package exec
+
+import (
+ "os/exec"
+)
+
+func BuildCommand(command string) *exec.Cmd {
+ return exec.Command("cmd.exe", "/C", command)
+}
+
+func WritePipe(pipe string, contents []byte) {
+ log.Fatal("fifos are not available on windows")
+}
+
+func GetPipe(dir, filename string) string {
+ log.Fatal("fifos are not available on windows")
+ return ""
+}
+
+func SwitchUser(username string) {
+ log.Fatal("user switching not available on windows")
+}
diff --git a/cmd/cmd/sops/subcommand/filestatus/filestatus.go b/cmd/cmd/sops/subcommand/filestatus/filestatus.go
new file mode 100644
index 000000000..3d40c96fa
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/filestatus/filestatus.go
@@ -0,0 +1,60 @@
+package filestatus
+
+import (
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+)
+
+// Opts represent the input options for FileStatus
+type Opts struct {
+ InputStore sops.Store
+ InputPath string
+}
+
+// Status represents the status of a file
+type Status struct {
+ // Encrypted represents whether the file provided is encrypted by SOPS
+ Encrypted bool `json:"encrypted"`
+}
+
+// FileStatus checks encryption status of a file
+func FileStatus(opts Opts) (Status, error) {
+ encrypted, err := cfs(opts.InputStore, opts.InputPath)
+ if err != nil {
+ return Status{}, fmt.Errorf("cannot check file status: %w", err)
+ }
+ return Status{Encrypted: encrypted}, nil
+}
+
+// cfs checks and reports on file encryption status.
+//
+// It tries to decrypt the input file with the provided store.
+// It returns true if the file contains sops metadata, false
+// if it doesn't or Version or MessageAuthenticationCode are
+// not found.
+// It reports any error encountered different from
+// sops.MetadataNotFound, as that is used to detect a sops
+// encrypted file.
+func cfs(s sops.Store, inputpath string) (bool, error) {
+ tree, err := common.LoadEncryptedFile(s, inputpath)
+ if err != nil && err == sops.MetadataNotFound {
+ return false, nil
+ }
+ if err != nil {
+ return false, fmt.Errorf("cannot load encrypted file: %w", err)
+ }
+
+ // NOTE: even if it's a file that sops recognize as containing
+ // valid metadata, we want to ensure some metadata are present
+ // to report the file as encrypted.
+ if tree.Metadata.Version == "" {
+ return false, nil
+ }
+ if tree.Metadata.MessageAuthenticationCode == "" {
+ return false, nil
+ }
+
+ return true, nil
+}
diff --git a/cmd/cmd/sops/subcommand/filestatus/filestatus_internal_test.go b/cmd/cmd/sops/subcommand/filestatus/filestatus_internal_test.go
new file mode 100644
index 000000000..c058a506d
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/filestatus/filestatus_internal_test.go
@@ -0,0 +1,52 @@
+package filestatus
+
+import (
+ "path"
+ "testing"
+
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/config"
+ "github.com/stretchr/testify/require"
+)
+
+const repoRoot = "../../../../"
+
+func fromRepoRoot(p string) string {
+ return path.Join(repoRoot, p)
+}
+
+func TestFileStatus(t *testing.T) {
+ tests := []struct {
+ name string
+ file string
+ expectedEncrypted bool
+ }{
+ {
+ name: "encrypted file should be reported as such",
+ file: "example.yaml",
+ expectedEncrypted: true,
+ },
+ {
+ name: "plain text file should be reported as cleartext",
+ file: "functional-tests/res/plainfile.yaml",
+ },
+ {
+ name: "file without mac should be reported as cleartext",
+ file: "functional-tests/res/plainfile.yaml",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := fromRepoRoot(tt.file)
+ s := common.DefaultStoreForPath(config.NewStoresConfig(), f)
+ encrypted, err := cfs(s, f)
+ require.Nil(t, err, "should not error")
+ if tt.expectedEncrypted {
+ require.True(t, encrypted, "file should have been reported as encrypted")
+ } else {
+ require.False(t, encrypted, "file should have been reported as cleartext")
+ }
+ })
+ }
+}
diff --git a/cmd/cmd/sops/subcommand/groups/add.go b/cmd/cmd/sops/subcommand/groups/add.go
new file mode 100644
index 000000000..716d83ea6
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/groups/add.go
@@ -0,0 +1,54 @@
+package groups
+
+import (
+ "os"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+)
+
+// AddOpts are the options for adding a key group to a SOPS file
+type AddOpts struct {
+ InputPath string
+ InputStore sops.Store
+ OutputStore sops.Store
+ Group sops.KeyGroup
+ GroupThreshold int
+ InPlace bool
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+// Add adds a key group to a SOPS file
+func Add(opts AddOpts) error {
+ tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath)
+ if err != nil {
+ return err
+ }
+ dataKey, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
+ if err != nil {
+ return err
+ }
+ tree.Metadata.KeyGroups = append(tree.Metadata.KeyGroups, opts.Group)
+
+ if opts.GroupThreshold != 0 {
+ tree.Metadata.ShamirThreshold = opts.GroupThreshold
+ }
+ tree.Metadata.UpdateMasterKeysWithKeyServices(dataKey, opts.KeyServices)
+ output, err := opts.OutputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return err
+ }
+ var outputFile = os.Stdout
+ if opts.InPlace {
+ var err error
+ outputFile, err = os.Create(opts.InputPath)
+ if err != nil {
+ return err
+ }
+ defer outputFile.Close()
+ }
+ outputFile.Write(output)
+ return nil
+}
diff --git a/cmd/cmd/sops/subcommand/groups/delete.go b/cmd/cmd/sops/subcommand/groups/delete.go
new file mode 100644
index 000000000..67f32ef25
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/groups/delete.go
@@ -0,0 +1,63 @@
+package groups
+
+import (
+ "os"
+
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+)
+
+// DeleteOpts are the options for deleting a key group from a SOPS file
+type DeleteOpts struct {
+ InputPath string
+ InputStore sops.Store
+ OutputStore sops.Store
+ Group uint
+ GroupThreshold int
+ InPlace bool
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+// Delete deletes a key group from a SOPS file
+func Delete(opts DeleteOpts) error {
+ tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath)
+ if err != nil {
+ return err
+ }
+ dataKey, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
+ if err != nil {
+ return err
+ }
+ tree.Metadata.KeyGroups = append(tree.Metadata.KeyGroups[:opts.Group], tree.Metadata.KeyGroups[opts.Group+1:]...)
+
+ if opts.GroupThreshold != 0 {
+ tree.Metadata.ShamirThreshold = opts.GroupThreshold
+ }
+
+ if len(tree.Metadata.KeyGroups) < tree.Metadata.ShamirThreshold {
+ return fmt.Errorf("removing this key group will make the Shamir threshold impossible to satisfy: "+
+ "Shamir threshold is %d, but we only have %d key groups", tree.Metadata.ShamirThreshold,
+ len(tree.Metadata.KeyGroups))
+ }
+
+ tree.Metadata.UpdateMasterKeysWithKeyServices(dataKey, opts.KeyServices)
+ output, err := opts.OutputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return err
+ }
+ var outputFile = os.Stdout
+ if opts.InPlace {
+ var err error
+ outputFile, err = os.Create(opts.InputPath)
+ if err != nil {
+ return err
+ }
+ defer outputFile.Close()
+ }
+ outputFile.Write(output)
+ return nil
+}
diff --git a/cmd/cmd/sops/subcommand/keyservice/keyservice.go b/cmd/cmd/sops/subcommand/keyservice/keyservice.go
new file mode 100644
index 000000000..c28f63690
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/keyservice/keyservice.go
@@ -0,0 +1,52 @@
+package keyservice
+
+import (
+ "net"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/logging"
+
+ "github.com/sirupsen/logrus"
+ "google.golang.org/grpc"
+)
+
+var log *logrus.Logger
+
+func init() {
+ log = logging.NewLogger("KEYSERVICE")
+}
+
+// Opts are the options the key service server can take
+type Opts struct {
+ Network string
+ Address string
+ Prompt bool
+}
+
+// Run runs a SOPS key service server
+func Run(opts Opts) error {
+ lis, err := net.Listen(opts.Network, opts.Address)
+ if err != nil {
+ return err
+ }
+ defer lis.Close()
+ grpcServer := grpc.NewServer()
+ keyservice.RegisterKeyServiceServer(grpcServer, keyservice.Server{
+ Prompt: opts.Prompt,
+ })
+ log.Infof("Listening on %s://%s", opts.Network, opts.Address)
+
+ // Close socket if we get killed
+ sigc := make(chan os.Signal, 1)
+ signal.Notify(sigc, os.Interrupt, os.Kill, syscall.SIGTERM)
+ go func(c chan os.Signal) {
+ sig := <-c
+ log.Infof("Caught signal %s: shutting down.", sig)
+ lis.Close()
+ os.Exit(0)
+ }(sigc)
+ return grpcServer.Serve(lis)
+}
diff --git a/cmd/cmd/sops/subcommand/publish/publish.go b/cmd/cmd/sops/subcommand/publish/publish.go
new file mode 100644
index 000000000..aed1118de
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/publish/publish.go
@@ -0,0 +1,189 @@
+package publish
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/config"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/logging"
+ "github.com/getsops/sops/v3/publish"
+ "github.com/getsops/sops/v3/version"
+
+ "github.com/sirupsen/logrus"
+)
+
+var log *logrus.Logger
+
+func init() {
+ log = logging.NewLogger("PUBLISH")
+}
+
+// Opts represents publish options and config
+type Opts struct {
+ Interactive bool
+ Cipher sops.Cipher
+ ConfigPath string
+ InputPath string
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+ InputStore sops.Store
+ OmitExtensions bool
+ Recursive bool
+ RootPath string
+}
+
+// Run publish operation
+func Run(opts Opts) error {
+ var fileContents []byte
+ path, err := filepath.Abs(opts.InputPath)
+ if err != nil {
+ return err
+ }
+
+ conf, err := config.LoadDestinationRuleForFile(opts.ConfigPath, opts.InputPath, make(map[string]*string))
+ if err != nil {
+ return err
+ }
+ if conf.Destination == nil {
+ return errors.New("no destination configured for this file")
+ }
+
+ var destinationPath string
+ if opts.Recursive {
+ destinationPath, err = filepath.Rel(opts.RootPath, opts.InputPath)
+ if err != nil {
+ return err
+ }
+ } else {
+ _, destinationPath = filepath.Split(path)
+ }
+ if opts.OmitExtensions || conf.OmitExtensions {
+ destinationPath = strings.TrimSuffix(destinationPath, filepath.Ext(path))
+ }
+
+ // Check that this is a sops-encrypted file
+ tree, err := common.LoadEncryptedFile(opts.InputStore, opts.InputPath)
+ if err != nil {
+ return err
+ }
+
+ data := map[string]interface{}{}
+
+ switch conf.Destination.(type) {
+ case *publish.S3Destination, *publish.GCSDestination:
+ // Re-encrypt if settings exist to do so
+ if len(conf.KeyGroups[0]) != 0 {
+ log.Debug("Re-encrypting tree before publishing")
+ _, err = common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: false,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return err
+ }
+
+ diffs := common.DiffKeyGroups(tree.Metadata.KeyGroups, conf.KeyGroups)
+ keysWillChange := false
+ for _, diff := range diffs {
+ if len(diff.Added) > 0 || len(diff.Removed) > 0 {
+ keysWillChange = true
+ }
+ }
+ if keysWillChange {
+ fmt.Printf("The following changes will be made to the file's key groups:\n")
+ common.PrettyPrintDiffs(diffs)
+ }
+
+ tree.Metadata = sops.Metadata{
+ KeyGroups: conf.KeyGroups,
+ UnencryptedSuffix: conf.UnencryptedSuffix,
+ EncryptedSuffix: conf.EncryptedSuffix,
+ Version: version.Version,
+ ShamirThreshold: conf.ShamirThreshold,
+ }
+
+ dataKey, errs := tree.GenerateDataKeyWithKeyServices(opts.KeyServices)
+ if len(errs) > 0 {
+ err = fmt.Errorf("Could not generate data key: %s", errs)
+ return err
+ }
+
+ err = common.EncryptTree(common.EncryptTreeOpts{
+ DataKey: dataKey,
+ Tree: tree,
+ Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return err
+ }
+
+ fileContents, err = opts.InputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ } else {
+ fileContents, err = os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("could not read file: %s", err)
+ }
+ }
+ case *publish.VaultDestination:
+ _, err = common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: false,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return err
+ }
+ data, err = sops.EmitAsMap(tree.Branches)
+ if err != nil {
+ return err
+ }
+ }
+
+ if opts.Interactive {
+ var response string
+ for response != "y" && response != "n" {
+ fmt.Printf("uploading %s to %s ? (y/n): ", path, conf.Destination.Path(destinationPath))
+ _, err := fmt.Scanln(&response)
+ if err != nil {
+ return err
+ }
+ }
+ if response == "n" {
+ msg := fmt.Sprintf("Publication of %s canceled", path)
+ if opts.Recursive {
+ fmt.Println(msg)
+ return nil
+ } else {
+ return errors.New(msg)
+ }
+ }
+ }
+
+ switch dest := conf.Destination.(type) {
+ case *publish.S3Destination, *publish.GCSDestination:
+ err = dest.Upload(fileContents, destinationPath)
+ case *publish.VaultDestination:
+ err = dest.UploadUnencrypted(data, destinationPath)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/cmd/cmd/sops/subcommand/updatekeys/updatekeys.go b/cmd/cmd/sops/subcommand/updatekeys/updatekeys.go
new file mode 100644
index 000000000..4b01e8ab7
--- /dev/null
+++ b/cmd/cmd/sops/subcommand/updatekeys/updatekeys.go
@@ -0,0 +1,125 @@
+package updatekeys
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/config"
+ "github.com/getsops/sops/v3/keyservice"
+)
+
+// Opts represents key operation options and config
+type Opts struct {
+ InputPath string
+ GroupQuorum int
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+ Interactive bool
+ ConfigPath string
+ InputType string
+}
+
+// UpdateKeys update the keys for a given file
+func UpdateKeys(opts Opts) error {
+ path, err := filepath.Abs(opts.InputPath)
+ if err != nil {
+ return err
+ }
+ info, err := os.Stat(path)
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return fmt.Errorf("can't operate on a directory")
+ }
+ opts.InputPath = path
+ return updateFile(opts)
+}
+
+func updateFile(opts Opts) error {
+ sc, err := config.LoadStoresConfig(opts.ConfigPath)
+ if err != nil {
+ return err
+ }
+ store := common.DefaultStoreForPath(sc, opts.InputPath)
+ log.Printf("Syncing keys for file %s", opts.InputPath)
+ tree, err := common.LoadEncryptedFile(store, opts.InputPath)
+ if err != nil {
+ return err
+ }
+ conf, err := config.LoadCreationRuleForFile(opts.ConfigPath, opts.InputPath, make(map[string]*string))
+ if err != nil {
+ return err
+ }
+ if conf == nil {
+ return fmt.Errorf("The config file %s does not contain any creation rule", opts.ConfigPath)
+ }
+
+ diffs := common.DiffKeyGroups(tree.Metadata.KeyGroups, conf.KeyGroups)
+ keysWillChange := false
+ for _, diff := range diffs {
+ if len(diff.Added) > 0 || len(diff.Removed) > 0 {
+ keysWillChange = true
+ }
+ }
+ if !keysWillChange {
+ log.Printf("File %s already up to date", opts.InputPath)
+ return nil
+ }
+ fmt.Printf("The following changes will be made to the file's groups:\n")
+ common.PrettyPrintDiffs(diffs)
+
+ if opts.Interactive {
+ var response string
+ for response != "y" && response != "n" {
+ fmt.Printf("Is this okay? (y/n):")
+ _, err = fmt.Scanln(&response)
+ if err != nil {
+ return err
+ }
+ }
+ if response == "n" {
+ log.Printf("File %s left unchanged", opts.InputPath)
+ return nil
+ }
+ }
+ key, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
+ if err != nil {
+ return common.NewExitError(err, codes.CouldNotRetrieveKey)
+ }
+ tree.Metadata.KeyGroups = conf.KeyGroups
+ if opts.GroupQuorum != 0 {
+ tree.Metadata.ShamirThreshold = opts.GroupQuorum
+ }
+ tree.Metadata.ShamirThreshold = min(tree.Metadata.ShamirThreshold, len(tree.Metadata.KeyGroups))
+ errs := tree.Metadata.UpdateMasterKeysWithKeyServices(key, opts.KeyServices)
+ if len(errs) > 0 {
+ return fmt.Errorf("error updating one or more master keys: %s", errs)
+ }
+ output, err := store.EmitEncryptedFile(*tree)
+ if err != nil {
+ return common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ outputFile, err := os.Create(opts.InputPath)
+ if err != nil {
+ return fmt.Errorf("could not open file for writing: %s", err)
+ }
+ defer outputFile.Close()
+ _, err = outputFile.Write(output)
+ if err != nil {
+ return fmt.Errorf("error writing to file: %s", err)
+ }
+ log.Printf("File %s synced with new keys", opts.InputPath)
+ return nil
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
diff --git a/cmd/cmd/sops/unset.go b/cmd/cmd/sops/unset.go
new file mode 100644
index 000000000..bb369748c
--- /dev/null
+++ b/cmd/cmd/sops/unset.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/cmd/sops/codes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/keyservice"
+)
+
+type unsetOpts struct {
+ Cipher sops.Cipher
+ InputStore sops.Store
+ OutputStore sops.Store
+ InputPath string
+ IgnoreMAC bool
+ TreePath []interface{}
+ KeyServices []keyservice.KeyServiceClient
+ DecryptionOrder []string
+}
+
+func unset(opts unsetOpts) ([]byte, error) {
+ // Load the file
+ tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
+ Cipher: opts.Cipher,
+ InputStore: opts.InputStore,
+ InputPath: opts.InputPath,
+ IgnoreMAC: opts.IgnoreMAC,
+ KeyServices: opts.KeyServices,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Decrypt the file
+ dataKey, err := common.DecryptTree(common.DecryptTreeOpts{
+ Cipher: opts.Cipher,
+ IgnoreMac: opts.IgnoreMAC,
+ Tree: tree,
+ KeyServices: opts.KeyServices,
+ DecryptionOrder: opts.DecryptionOrder,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Unset the value
+ newBranch, err := tree.Branches[0].Unset(opts.TreePath)
+ if err != nil {
+ return nil, err
+ }
+ tree.Branches[0] = newBranch
+
+ err = common.EncryptTree(common.EncryptTreeOpts{
+ DataKey: dataKey, Tree: tree, Cipher: opts.Cipher,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree)
+ if err != nil {
+ return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
+ }
+ return encryptedFile, err
+}
diff --git a/config/config.go b/config/config.go
index 22105c55f..9620c0da9 100644
--- a/config/config.go
+++ b/config/config.go
@@ -94,6 +94,7 @@ type configFile struct {
}
type keyGroup struct {
+ Merge []keyGroup
KMS []kmsKey
GCPKMS []gcpKmsKey `yaml:"gcp_kms"`
AzureKV []azureKVKey `yaml:"azure_keyvault"`
@@ -134,21 +135,23 @@ type destinationRule struct {
}
type creationRule struct {
- PathRegex string `yaml:"path_regex"`
- KMS string
- AwsProfile string `yaml:"aws_profile"`
- Age string `yaml:"age"`
- PGP string
- GCPKMS string `yaml:"gcp_kms"`
- AzureKeyVault string `yaml:"azure_keyvault"`
- VaultURI string `yaml:"hc_vault_transit_uri"`
- KeyGroups []keyGroup `yaml:"key_groups"`
- 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"`
- MACOnlyEncrypted bool `yaml:"mac_only_encrypted"`
+ PathRegex string `yaml:"path_regex"`
+ KMS string
+ AwsProfile string `yaml:"aws_profile"`
+ Age string `yaml:"age"`
+ PGP string
+ GCPKMS string `yaml:"gcp_kms"`
+ AzureKeyVault string `yaml:"azure_keyvault"`
+ VaultURI string `yaml:"hc_vault_transit_uri"`
+ KeyGroups []keyGroup `yaml:"key_groups"`
+ 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"`
+ UnencryptedCommentRegex string `yaml:"unencrypted_comment_regex"`
+ EncryptedCommentRegex string `yaml:"encrypted_comment_regex"`
+ MACOnlyEncrypted bool `yaml:"mac_only_encrypted"`
}
func NewStoresConfig() *StoresConfig {
@@ -169,49 +172,85 @@ func (f *configFile) load(bytes []byte) error {
// Config is the configuration for a given SOPS file
type Config struct {
- KeyGroups []sops.KeyGroup
- ShamirThreshold int
- UnencryptedSuffix string
- EncryptedSuffix string
- UnencryptedRegex string
- EncryptedRegex string
- MACOnlyEncrypted bool
- Destination publish.Destination
- OmitExtensions bool
+ KeyGroups []sops.KeyGroup
+ ShamirThreshold int
+ UnencryptedSuffix string
+ EncryptedSuffix string
+ UnencryptedRegex string
+ EncryptedRegex string
+ UnencryptedCommentRegex string
+ EncryptedCommentRegex string
+ MACOnlyEncrypted bool
+ Destination publish.Destination
+ OmitExtensions bool
+}
+
+func deduplicateKeygroup(group sops.KeyGroup) sops.KeyGroup {
+ var deduplicatedKeygroup sops.KeyGroup
+
+ unique := make(map[string]bool)
+ for _, v := range group {
+ key := fmt.Sprintf("%T/%v", v, v.ToString())
+ if _, ok := unique[key]; ok {
+ // key already contained, therefore not unique
+ continue
+ }
+
+ deduplicatedKeygroup = append(deduplicatedKeygroup, v)
+ unique[key] = true
+ }
+
+ return deduplicatedKeygroup
+}
+
+func extractMasterKeys(group keyGroup) (sops.KeyGroup, error) {
+ var keyGroup sops.KeyGroup
+ for _, k := range group.Merge {
+ subKeyGroup, err := extractMasterKeys(k)
+ if err != nil {
+ return nil, err
+ }
+ keyGroup = append(keyGroup, subKeyGroup...)
+ }
+
+ for _, k := range group.Age {
+ keys, err := age.MasterKeysFromRecipients(k)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ keyGroup = append(keyGroup, key)
+ }
+ }
+ for _, k := range group.PGP {
+ keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k))
+ }
+ for _, k := range group.KMS {
+ keyGroup = append(keyGroup, kms.NewMasterKeyWithProfile(k.Arn, k.Role, k.Context, k.AwsProfile))
+ }
+ for _, k := range group.GCPKMS {
+ keyGroup = append(keyGroup, gcpkms.NewMasterKeyFromResourceID(k.ResourceID))
+ }
+ for _, k := range group.AzureKV {
+ keyGroup = append(keyGroup, azkv.NewMasterKey(k.VaultURL, k.Key, k.Version))
+ }
+ for _, k := range group.Vault {
+ if masterKey, err := hcvault.NewMasterKeyFromURI(k); err == nil {
+ keyGroup = append(keyGroup, masterKey)
+ } else {
+ return nil, err
+ }
+ }
+ return deduplicateKeygroup(keyGroup), nil
}
func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) {
var groups []sops.KeyGroup
if len(cRule.KeyGroups) > 0 {
for _, group := range cRule.KeyGroups {
- var keyGroup sops.KeyGroup
- for _, k := range group.Age {
- keys, err := age.MasterKeysFromRecipients(k)
- if err != nil {
- return nil, err
- }
- for _, key := range keys {
- keyGroup = append(keyGroup, key)
- }
- }
- for _, k := range group.PGP {
- keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k))
- }
- for _, k := range group.KMS {
- keyGroup = append(keyGroup, kms.NewMasterKeyWithProfile(k.Arn, k.Role, k.Context, k.AwsProfile))
- }
- for _, k := range group.GCPKMS {
- keyGroup = append(keyGroup, gcpkms.NewMasterKeyFromResourceID(k.ResourceID))
- }
- for _, k := range group.AzureKV {
- keyGroup = append(keyGroup, azkv.NewMasterKey(k.VaultURL, k.Key, k.Version))
- }
- for _, k := range group.Vault {
- if masterKey, err := hcvault.NewMasterKeyFromURI(k); err == nil {
- keyGroup = append(keyGroup, masterKey)
- } else {
- return nil, err
- }
+ keyGroup, err := extractMasterKeys(group)
+ if err != nil {
+ return nil, err
}
groups = append(groups, keyGroup)
}
@@ -269,16 +308,6 @@ func loadConfigFile(confPath string) (*configFile, error) {
return conf, nil
}
-func loadConfigFromString(yamlString string) (*configFile, error) {
- conf := &configFile{}
- conf.Stores = *NewStoresConfig()
- err := conf.load([]byte(yamlString))
- if err != nil {
- return nil, fmt.Errorf("error loading config: %s", err)
- }
- return conf, nil
-}
-
func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) (*Config, error) {
cryptRuleCount := 0
if rule.UnencryptedSuffix != "" {
@@ -293,9 +322,15 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
if rule.EncryptedRegex != "" {
cryptRuleCount++
}
+ if rule.UnencryptedCommentRegex != "" {
+ cryptRuleCount++
+ }
+ if rule.EncryptedCommentRegex != "" {
+ cryptRuleCount++
+ }
if cryptRuleCount > 1 {
- return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, or unencrypted_regex for the same rule")
+ return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex for the same rule")
}
groups, err := getKeyGroupsFromCreationRule(rule, kmsEncryptionContext)
@@ -304,13 +339,15 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string)
}
return &Config{
- KeyGroups: groups,
- ShamirThreshold: rule.ShamirThreshold,
- UnencryptedSuffix: rule.UnencryptedSuffix,
- EncryptedSuffix: rule.EncryptedSuffix,
- UnencryptedRegex: rule.UnencryptedRegex,
- EncryptedRegex: rule.EncryptedRegex,
- MACOnlyEncrypted: rule.MACOnlyEncrypted,
+ KeyGroups: groups,
+ ShamirThreshold: rule.ShamirThreshold,
+ UnencryptedSuffix: rule.UnencryptedSuffix,
+ EncryptedSuffix: rule.EncryptedSuffix,
+ UnencryptedRegex: rule.UnencryptedRegex,
+ EncryptedRegex: rule.EncryptedRegex,
+ UnencryptedCommentRegex: rule.UnencryptedCommentRegex,
+ EncryptedCommentRegex: rule.EncryptedCommentRegex,
+ MACOnlyEncrypted: rule.MACOnlyEncrypted,
}, nil
}
@@ -420,15 +457,6 @@ func LoadCreationRuleForFile(confPath string, filePath string, kmsEncryptionCont
return parseCreationRuleForFile(conf, confPath, filePath, kmsEncryptionContext)
}
-func LoadCreationRuleFromConfigString(configString string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
- conf, err := loadConfigFromString(configString)
- if err != nil {
- return nil, err
- }
-
- return parseCreationRuleForFile(conf, filePath, filePath, kmsEncryptionContext)
-}
-
// LoadDestinationRuleForFile works the same as LoadCreationRuleForFile, but gets the "creation_rule" from the matching destination_rule's
// "recreation_rule".
func LoadDestinationRuleForFile(confPath string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
diff --git a/config/config_test.go b/config/config_test.go
index 8f4fb006b..abf2c66a5 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -120,6 +120,125 @@ creation_rules:
- 'https://baz.vault:8200/v1/baz/keys/baz-key'
`)
+var sampleConfigWithMergeType = []byte(`
+creation_rules:
+ - path_regex: ""
+ key_groups:
+ # key00
+ - hc_vault:
+ - 'https://foo.vault:8200/v1/foo/keys/foo-key'
+ - merge:
+ - merge:
+ - kms:
+ # key01
+ - arn: foo
+ aws_profile: foo
+ pgp:
+ # key02
+ - foo
+ gcp_kms:
+ # key03
+ - resource_id: foo
+ azure_keyvault:
+ # key04
+ - vaultUrl: https://foo.vault.azure.net
+ key: foo-key
+ version: fooversion
+ hc_vault:
+ # key05
+ - 'https://bar.vault:8200/v1/bar/keys/bar-key'
+ - kms:
+ # key06
+ - arn: bar
+ aws_profile: bar
+ pgp:
+ # key07
+ - bar
+ gcp_kms:
+ # key08
+ - resource_id: bar
+ # key09
+ - resource_id: baz
+ azure_keyvault:
+ # key10
+ - vaultUrl: https://bar.vault.azure.net
+ key: bar-key
+ version: barversion
+ hc_vault:
+ # key01 - duplicate#1
+ - 'https://baz.vault:8200/v1/baz/keys/baz-key'
+ kms:
+ # key11
+ - arn: baz
+ aws_profile: baz
+ pgp:
+ # key12
+ - baz
+ gcp_kms:
+ # key03 - duplicate#2
+ # --> should be removed when loading config
+ - resource_id: bar
+ azure_keyvault:
+ # key04 - duplicate#3
+ - vaultUrl: https://foo.vault.azure.net
+ key: foo-key
+ version: fooversion
+ hc_vault:
+ # key13 - duplicate#4 - but from different key_group
+ # --> should stay
+ - 'https://foo.vault:8200/v1/foo/keys/foo-key'
+ - kms:
+ # key14
+ - arn: qux
+ aws_profile: qux
+ # key14 - duplicate#5
+ - arn: baz
+ aws_profile: bar
+ pgp:
+ # key15
+ - qux
+ gcp_kms:
+ # key16
+ - resource_id: qux
+ # key17
+ - resource_id: fnord
+ azure_keyvault:
+ # key18
+ - vaultUrl: https://baz.vault.azure.net
+ key: baz-key
+ version: bazversion
+ hc_vault:
+ # key19
+ - 'https://qux.vault:8200/v1/qux/keys/qux-key'
+ # everything below this should be loaded,
+ # since it is not in a merge block
+ kms:
+ # duplicated key06
+ - arn: bar
+ aws_profile: bar
+ # key20
+ - arn: fnord
+ aws_profile: fnord
+ pgp:
+ # duplicated key07
+ - bar
+ gcp_kms:
+ # duplicated key08
+ - resource_id: bar
+ # key21
+ - resource_id: fnord
+ azure_keyvault:
+ # duplicated key10
+ - vaultUrl: https://bar.vault.azure.net
+ key: bar-key
+ version: barversion
+ hc_vault:
+ # duplicated 'key01 - duplicate#2'
+ - 'https://baz.vault:8200/v1/baz/keys/baz-key'
+ # key22
+ - 'https://fnord.vault:8200/v1/fnord/keys/fnord-key'
+`)
+
var sampleConfigWithSuffixParameters = []byte(`
creation_rules:
- path_regex: foobar*
@@ -166,6 +285,22 @@ creation_rules:
mac_only_encrypted: true
`)
+var sampleConfigWithEncryptedCommentRegexParameters = []byte(`
+creation_rules:
+ - path_regex: barbar*
+ kms: "1"
+ pgp: "2"
+ encrypted_comment_regex: "sops:enc"
+ `)
+
+var sampleConfigWithUnencryptedCommentRegexParameters = []byte(`
+creation_rules:
+ - path_regex: barbar*
+ kms: "1"
+ pgp: "2"
+ unencrypted_comment_regex: "sops:dec"
+ `)
+
var sampleConfigWithInvalidParameters = []byte(`
creation_rules:
- path_regex: foobar*
@@ -324,6 +459,14 @@ func TestLoadConfigFileWithGroups(t *testing.T) {
assert.Equal(t, expected, conf)
}
+func TestLoadConfigFileWithMerge(t *testing.T) {
+ conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithMergeType, t), "/conf/path", "whatever", nil)
+ assert.Nil(t, err)
+ assert.Equal(t, 2, len(conf.KeyGroups))
+ assert.Equal(t, 1, len(conf.KeyGroups[0]))
+ assert.Equal(t, 22, len(conf.KeyGroups[1]))
+}
+
func TestLoadConfigFileWithNoMatchingRules(t *testing.T) {
_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithNoMatchingRules, t), "/conf/path", "foobar2000", nil)
assert.NotNil(t, err)
@@ -430,6 +573,18 @@ func TestLoadConfigFileWithMACOnlyEncrypted(t *testing.T) {
assert.Equal(t, true, conf.MACOnlyEncrypted)
}
+func TestLoadConfigFileWithUnencryptedCommentRegex(t *testing.T) {
+ conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithUnencryptedCommentRegexParameters, t), "/conf/path", "barbar", nil)
+ assert.Equal(t, nil, err)
+ assert.Equal(t, "sops:dec", conf.UnencryptedCommentRegex)
+}
+
+func TestLoadConfigFileWithEncryptedCommentRegex(t *testing.T) {
+ conf, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithEncryptedCommentRegexParameters, t), "/conf/path", "barbar", nil)
+ assert.Equal(t, nil, err)
+ assert.Equal(t, "sops:enc", conf.EncryptedCommentRegex)
+}
+
func TestLoadConfigFileWithInvalidParameters(t *testing.T) {
_, err := parseCreationRuleForFile(parseConfigFile(sampleConfigWithInvalidParameters, t), "/conf/path", "foobar", nil)
assert.NotNil(t, err)
diff --git a/decrypt/decrypt/decrypt.go b/decrypt/decrypt/decrypt.go
new file mode 100644
index 000000000..e26cbe479
--- /dev/null
+++ b/decrypt/decrypt/decrypt.go
@@ -0,0 +1,80 @@
+/*
+Package decrypt is the external API other Go programs can use to decrypt SOPS files. It is the only package in SOPS with
+a stable API.
+*/
+package decrypt // import "github.com/getsops/sops/v3/decrypt"
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/getsops/sops/v3/aes"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ . "github.com/getsops/sops/v3/cmd/sops/formats" // Re-export
+ "github.com/getsops/sops/v3/config"
+)
+
+// File is a wrapper around Data that reads a local encrypted
+// file and returns its cleartext data in an []byte
+func File(path, format string) (cleartext []byte, err error) {
+ // Read the file into an []byte
+ encryptedData, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to read %q: %w", path, err)
+ }
+
+ // uses same logic as cli.
+ formatFmt := FormatForPathOrString(path, format)
+ return DataWithFormat(encryptedData, formatFmt)
+}
+
+// DataWithFormat is a helper that takes encrypted data, and a format enum value,
+// decrypts the data and returns its cleartext in an []byte.
+func DataWithFormat(data []byte, format Format) (cleartext []byte, err error) {
+
+ store := common.StoreForFormat(format, config.NewStoresConfig())
+
+ // Load SOPS file and access the data key
+ tree, err := store.LoadEncryptedFile(data)
+ if err != nil {
+ return nil, err
+ }
+ key, err := tree.Metadata.GetDataKey()
+ if err != nil {
+ return nil, err
+ }
+
+ // Decrypt the tree
+ cipher := aes.NewCipher()
+ mac, err := tree.Decrypt(key, cipher)
+ if err != nil {
+ return nil, err
+ }
+
+ // Compute the hash of the cleartext tree and compare it with
+ // the one that was stored in the document. If they match,
+ // integrity was preserved
+ originalMac, err := cipher.Decrypt(
+ tree.Metadata.MessageAuthenticationCode,
+ key,
+ tree.Metadata.LastModified.Format(time.RFC3339),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("Failed to decrypt original mac: %w", err)
+ }
+ if originalMac != mac {
+ return nil, fmt.Errorf("Failed to verify data integrity. expected mac %q, got %q", originalMac, mac)
+ }
+
+ return store.EmitPlainFile(tree.Branches)
+}
+
+// Data is a helper that takes encrypted data and a format string,
+// decrypts the data and returns its cleartext in an []byte.
+// The format string can be `json`, `yaml`, `ini`, `dotenv` or `binary`.
+// If the format string is empty, binary format is assumed.
+func Data(data []byte, format string) (cleartext []byte, err error) {
+ formatFmt := FormatFromString(format)
+ return DataWithFormat(data, formatFmt)
+}
diff --git a/decrypt/decrypt/example_test.go b/decrypt/decrypt/example_test.go
new file mode 100644
index 000000000..dcdd33211
--- /dev/null
+++ b/decrypt/decrypt/example_test.go
@@ -0,0 +1,55 @@
+package decrypt
+
+import (
+ "encoding/json"
+
+ "github.com/getsops/sops/v3/logging"
+
+ "github.com/sirupsen/logrus"
+)
+
+var log *logrus.Logger
+
+func init() {
+ log = logging.NewLogger("DECRYPT")
+}
+
+type configuration struct {
+ FirstName string `json:"firstName"`
+ LastName string `json:"lastName"`
+ Age float64 `json:"age"`
+ Address struct {
+ City string `json:"city"`
+ PostalCode string `json:"postalCode"`
+ State string `json:"state"`
+ StreetAddress string `json:"streetAddress"`
+ } `json:"address"`
+ PhoneNumbers []struct {
+ Number string `json:"number"`
+ Type string `json:"type"`
+ } `json:"phoneNumbers"`
+ AnEmptyValue string `json:"anEmptyValue"`
+}
+
+func ExampleFile() {
+ var (
+ confPath string = "./example.json"
+ cfg configuration
+ err error
+ )
+ confData, err := File(confPath, "json")
+ if err != nil {
+ log.Fatalf("cleartext configuration marshalling failed with error: %v", err)
+ }
+ err = json.Unmarshal(confData, &cfg)
+ if err != nil {
+ log.Fatalf("cleartext configuration unmarshalling failed with error: %v", err)
+ }
+ if cfg.FirstName != "John" ||
+ cfg.LastName != "Smith" ||
+ cfg.Age != 25.4 ||
+ cfg.PhoneNumbers[1].Number != "646 555-4567" {
+ log.Fatalf("configuration does not contain expected values: %+v", cfg)
+ }
+ log.Printf("%+v", cfg)
+}
diff --git a/docs/docs/images/cncf-color-bg.svg b/docs/docs/images/cncf-color-bg.svg
new file mode 100644
index 000000000..c8d667f37
--- /dev/null
+++ b/docs/docs/images/cncf-color-bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/docs/release.md b/docs/docs/release.md
new file mode 100644
index 000000000..7485b136a
--- /dev/null
+++ b/docs/docs/release.md
@@ -0,0 +1,75 @@
+# Release procedure
+
+This document describes the procedure for releasing a new version of SOPS. It
+is intended for maintainers of the project, but may be useful for anyone
+interested in the release process.
+
+## Overview
+
+The release is performed by creating a signed tag for the release, and pushing
+it to GitHub. This will automatically trigger a GitHub Actions workflow that
+builds the binaries, packages, SBOMs, and other artifacts for the release
+using [GoReleaser](https://goreleaser.com), and uploads them to GitHub.
+
+The configuration for GoReleaser is in the file
+[`.goreleaser.yaml`](../.goreleaser.yaml). The configuration for the GitHub
+Actions workflow is in the file
+[`release.yml`](../.github/workflows/release.yml).
+
+This configuration is quite sophisticated, and ensures at least the following:
+
+- The release is built for multiple platforms and architectures, including
+ Linux, macOS, and Windows, and for both AMD64 and ARM64.
+- The release includes multiple packages in Debian and RPM formats.
+- For every binary, a corresponding SBOM is generated and published.
+- For all binaries, a checksum file is generated and signed using
+ [Cosign](https://docs.sigstore.dev/cosign/overview/) with GitHub OIDC.
+- Both Debian and Alpine Docker multi-arch images are built and pushed to GitHub
+ Container Registry and Quay.io.
+- The container images are signed using
+ [Cosign](https://docs.sigstore.dev/cosign/overview/) with GitHub OIDC.
+- [SLSA provenance](https://slsa.dev/provenance/v0.2) metadata is generated for
+ release artifacts and container images.
+
+## Preparation
+
+- [ ] Ensure that all changes intended for the release are merged into the
+ `main` branch. At present, this means that all pull requests attached to the
+ milestone for the release are merged. If there are any pull requests that
+ should not be included in the release, move them to a different milestone.
+- [ ] Create a pull request to update the [`CHANGELOG.rst`](../CHANGELOG.rst)
+ file. This should include a summary of all changes since the last release,
+ including references to any relevant pull requests.
+- [ ] In this same pull request, update the version number in `version/version.go`
+ to the new version number.
+- [ ] Get approval for the pull request from at least one other maintainer, and
+ merge it into `main`.
+- [ ] Ensure CI passes on the `main` branch.
+
+## Release
+
+- [ ] Ensure your local copy of the `main` branch is up-to-date:
+
+ ```sh
+ git checkout main
+ git pull
+ ```
+
+- [ ] Create a **signed tag** for the release, using the following command:
+
+ ```sh
+ git tag -s -m
+ ```
+
+ where `` is the version number of the release. The version number
+ should be in the form `vX.Y.Z`, where `X`, `Y`, and `Z` are integers. The
+ version number should be incremented according to
+ [semantic versioning](https://semver.org/).
+- [ ] Push the tag to GitHub:
+
+ ```sh
+ git push origin
+ ```
+
+- [ ] Ensure the release is built successfully on GitHub Actions. This will
+ automatically create a release on GitHub.
diff --git a/functional-tests/res/plainfile.yaml b/functional-tests/res/plainfile.yaml
new file mode 100644
index 000000000..bb56b0551
--- /dev/null
+++ b/functional-tests/res/plainfile.yaml
@@ -0,0 +1 @@
+hello: world
diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs
index 46703a459..cca673edb 100644
--- a/functional-tests/src/lib.rs
+++ b/functional-tests/src/lib.rs
@@ -273,12 +273,12 @@ bar: baz",
.arg(r#"{"aa": "aaa"}"#)
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
@@ -317,12 +317,12 @@ bar: baz",
.arg(r#"{"cc": "ccc"}"#)
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
@@ -365,12 +365,12 @@ b: ba"#
.arg(r#"{"aa": "aaa"}"#)
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
@@ -413,12 +413,12 @@ b: ba"#
.arg(r#"{"cc": "ccc"}"#)
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
@@ -472,12 +472,12 @@ b: ba"#
.arg(file_path.clone())
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
@@ -529,12 +529,12 @@ b: ba"#
.arg(file_path.clone())
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
let mut s = String::new();
File::open(file_path)
.unwrap()
@@ -549,6 +549,224 @@ b: ba"#
}
}
+ #[test]
+ fn unset_json_file() {
+ // Test removal of tree branch
+ let file_path =
+ prepare_temp_file("test_unset.json", r#"{"a": 2, "b": "ba", "c": [1,2]}"#.as_bytes());
+ assert!(
+ Command::new(SOPS_BINARY_PATH)
+ .arg("encrypt")
+ .arg("-i")
+ .arg(file_path.clone())
+ .output()
+ .expect("Error running sops")
+ .status
+ .success(),
+ "sops didn't exit successfully"
+ );
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg(file_path.clone())
+ .arg(r#"["a"]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut content)
+ .unwrap();
+ let data: Value = serde_json::from_str(&content).expect("Error parsing sops's JSON output");
+ if let Value::Mapping(data) = data {
+ assert!(!data.contains_key(&Value::String("a".to_owned())));
+ assert!(data.contains_key(&Value::String("b".to_owned())));
+ } else {
+ panic!("Output JSON does not have the expected structure");
+ }
+
+ // Test idempotent unset
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg("--idempotent")
+ .arg(file_path.clone())
+ .arg(r#"["a"]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut idempotent_content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut idempotent_content)
+ .unwrap();
+ assert!(idempotent_content == content);
+
+ // Test removal of list item
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg(file_path.clone())
+ .arg(r#"["c"][1]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut content)
+ .unwrap();
+ let data: Value = serde_json::from_str(&content).expect("Error parsing sops's JSON output");
+ if let Value::Mapping(data) = data {
+ assert_eq!(data["c"].as_sequence().unwrap().len(), 1);
+ } else {
+ panic!("Output JSON does not have the expected structure");
+ }
+
+ // Test idempotent unset list item
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg("--idempotent")
+ .arg(file_path.clone())
+ .arg(r#"["c"][1]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut idempotent_content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut idempotent_content)
+ .unwrap();
+ assert!(idempotent_content == content);
+ }
+
+ #[test]
+ fn unset_yaml_file() {
+ // Test removal of tree branch
+ let file_path =
+ prepare_temp_file("test_unset.yaml", r#"{"a": 2, "b": "ba", "c": [1,2]}"#.as_bytes());
+ assert!(
+ Command::new(SOPS_BINARY_PATH)
+ .arg("encrypt")
+ .arg("-i")
+ .arg(file_path.clone())
+ .output()
+ .expect("Error running sops")
+ .status
+ .success(),
+ "sops didn't exit successfully"
+ );
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg(file_path.clone())
+ .arg(r#"["a"]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut content)
+ .unwrap();
+ let data: Value = serde_yaml::from_str(&content).expect("Error parsing sops's YAML output");
+ if let Value::Mapping(data) = data {
+ assert!(!data.contains_key(&Value::String("a".to_owned())));
+ assert!(data.contains_key(&Value::String("b".to_owned())));
+ } else {
+ panic!("Output YAML does not have the expected structure");
+ }
+
+ // Test idempotent unset
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg("--idempotent")
+ .arg(file_path.clone())
+ .arg(r#"["a"]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut idempotent_content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut idempotent_content)
+ .unwrap();
+ assert!(idempotent_content == content);
+
+ // Test removal of list item
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg(file_path.clone())
+ .arg(r#"["c"][0]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut content)
+ .unwrap();
+ let data: Value = serde_yaml::from_str(&content).expect("Error parsing sops's YAML output");
+ if let Value::Mapping(data) = data {
+ assert_eq!(data["c"].as_sequence().unwrap().len(), 1);
+ } else {
+ panic!("Output YAML does not have the expected structure");
+ }
+
+ // Test idempotent unset list item
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("unset")
+ .arg("--idempotent")
+ .arg(file_path.clone())
+ .arg(r#"["c"][1]"#)
+ .output()
+ .expect("Error running sops");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(output.status.success(), "sops didn't exit successfully");
+ let mut idempotent_content = String::new();
+ File::open(file_path.clone())
+ .unwrap()
+ .read_to_string(&mut idempotent_content)
+ .unwrap();
+ assert!(idempotent_content == content);
+ }
+
#[test]
fn decrypt_file_no_mac() {
let file_path = prepare_temp_file(
@@ -876,12 +1094,12 @@ echo -E "${foo}"
.arg(format!("/bin/bash {}", print_foo))
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
assert_eq!(String::from_utf8_lossy(&output.stdout), "bar\n");
let print_bar = prepare_temp_file(
"print_bar.sh",
@@ -896,13 +1114,13 @@ echo -E "${bar}"
.arg(format!("/bin/bash {}", print_bar))
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
- assert_eq!(String::from_utf8_lossy(&output.stdout), "baz\\nbam\n");
+ assert!(output.status.success(), "sops didn't exit successfully");
+ assert_eq!(String::from_utf8_lossy(&output.stdout), "baz\nbam\n");
}
#[test]
@@ -935,12 +1153,12 @@ bar: |-
.arg("cat {}")
.output()
.expect("Error running sops");
- assert!(output.status.success(), "sops didn't exit successfully");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
+ assert!(output.status.success(), "sops didn't exit successfully");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
r#"{
@@ -949,4 +1167,47 @@ bar: |-
}"#
);
}
+
+ #[test]
+ fn exec_file_filename() {
+ let file_path = prepare_temp_file(
+ "test_exec_file_filename.yaml",
+ r#"foo: bar
+bar: |-
+ baz
+ bam
+"#
+ .as_bytes(),
+ );
+ assert!(
+ Command::new(SOPS_BINARY_PATH)
+ .arg("-e")
+ .arg("-i")
+ .arg(file_path.clone())
+ .output()
+ .expect("Error running sops")
+ .status
+ .success(),
+ "sops didn't exit successfully"
+ );
+ let output = Command::new(SOPS_BINARY_PATH)
+ .arg("exec-file")
+ .arg("--no-fifo")
+ .arg("--filename")
+ .arg("foobar")
+ .arg(file_path.clone())
+ .arg("echo {}")
+ .output()
+ .expect("Error running sops");
+ assert!(output.status.success(), "sops didn't exit successfully");
+ println!(
+ "stdout: {}, stderr: {}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(
+ String::from_utf8_lossy(&output.stdout).ends_with("foobar\n"),
+ "filename did not end with 'foobar'"
+ );
+ }
}
diff --git a/gcpkms/gcpkms/keysource.go b/gcpkms/gcpkms/keysource.go
new file mode 100644
index 000000000..903db5254
--- /dev/null
+++ b/gcpkms/gcpkms/keysource.go
@@ -0,0 +1,280 @@
+package gcpkms // import "github.com/getsops/sops/v3/gcpkms"
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+
+ kms "cloud.google.com/go/kms/apiv1"
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/oauth2"
+ "google.golang.org/api/option"
+ "google.golang.org/grpc"
+
+ "github.com/getsops/sops/v3/logging"
+)
+
+const (
+ // SopsGoogleCredentialsEnv can be set as an environment variable as either
+ // a path to a credentials file, or directly as the variable's value in JSON
+ // format.
+ SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS"
+ SopsGoogleCredentialsAccessToken = "CLOUDSDK_AUTH_ACCESS_TOKEN"
+ // KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey.
+ KeyTypeIdentifier = "gcp_kms"
+)
+
+var (
+ // gcpkmsTTL is the duration after which a MasterKey requires rotation.
+ gcpkmsTTL = time.Hour * 24 * 30 * 6
+ // log is the global logger for any GCP KMS MasterKey.
+ log *logrus.Logger
+)
+
+func init() {
+ log = logging.NewLogger("GCPKMS")
+}
+
+// MasterKey is a GCP KMS key used to encrypt and decrypt the SOPS
+// data key.
+type MasterKey struct {
+ // ResourceID is the resource id used to refer to the gcp kms key.
+ // It can be retrieved using the `gcloud` command.
+ ResourceID string
+ // EncryptedKey is the string returned after encrypting with GCP KMS.
+ EncryptedKey string
+ // CreationDate is the creation timestamp of the MasterKey. Used
+ // for NeedsRotation.
+ CreationDate time.Time
+
+ // credentialJSON is the Service Account credentials JSON used for
+ // authenticating towards the GCP KMS service.
+ credentialJSON []byte
+ // grpcConn can be used to inject a custom GCP client connection.
+ // Mostly useful for testing at present, to wire the client to a mock
+ // server.
+ grpcConn *grpc.ClientConn
+}
+
+// NewMasterKeyFromResourceID creates a new MasterKey with the provided resource
+// ID.
+func NewMasterKeyFromResourceID(resourceID string) *MasterKey {
+ k := &MasterKey{}
+ resourceID = strings.Replace(resourceID, " ", "", -1)
+ k.ResourceID = resourceID
+ k.CreationDate = time.Now().UTC()
+ return k
+}
+
+// MasterKeysFromResourceIDString takes a comma separated list of GCP KMS
+// resource IDs and returns a slice of new MasterKeys for them.
+func MasterKeysFromResourceIDString(resourceID string) []*MasterKey {
+ var keys []*MasterKey
+ if resourceID == "" {
+ return keys
+ }
+ for _, s := range strings.Split(resourceID, ",") {
+ keys = append(keys, NewMasterKeyFromResourceID(s))
+ }
+ return keys
+}
+
+// CredentialJSON is the Service Account credentials JSON used for authenticating
+// towards the GCP KMS service.
+type CredentialJSON []byte
+
+// ApplyToMasterKey configures the CredentialJSON on the provided key.
+func (c CredentialJSON) ApplyToMasterKey(key *MasterKey) {
+ key.credentialJSON = c
+}
+
+// Encrypt takes a SOPS data key, encrypts it with GCP KMS, and stores the
+// result in the EncryptedKey field.
+func (key *MasterKey) Encrypt(dataKey []byte) error {
+ service, err := key.newKMSClient()
+ if err != nil {
+ log.WithField("resourceID", key.ResourceID).Info("Encryption failed")
+ return fmt.Errorf("cannot create GCP KMS service: %w", err)
+ }
+ defer func() {
+ if err := service.Close(); err != nil {
+ log.Error("failed to close GCP KMS client connection")
+ }
+ }()
+
+ req := &kmspb.EncryptRequest{
+ Name: key.ResourceID,
+ Plaintext: dataKey,
+ }
+ ctx := context.Background()
+ resp, err := service.Encrypt(ctx, req)
+ if err != nil {
+ log.WithField("resourceID", key.ResourceID).Info("Encryption failed")
+ return fmt.Errorf("failed to encrypt sops data key with GCP KMS key: %w", err)
+ }
+ // NB: base64 encoding is for compatibility with SOPS <=3.8.x.
+ // The previous GCP KMS client used to work with base64 encoded
+ // strings.
+ key.EncryptedKey = base64.StdEncoding.EncodeToString(resp.Ciphertext)
+ log.WithField("resourceID", key.ResourceID).Info("Encryption succeeded")
+ return nil
+}
+
+// SetEncryptedDataKey sets the encrypted data key for this master key.
+func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
+ key.EncryptedKey = string(enc)
+}
+
+// EncryptedDataKey returns the encrypted data key this master key holds.
+func (key *MasterKey) EncryptedDataKey() []byte {
+ return []byte(key.EncryptedKey)
+}
+
+// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
+// encrypted yet.
+func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
+ if key.EncryptedKey == "" {
+ return key.Encrypt(dataKey)
+ }
+ return nil
+}
+
+// Decrypt decrypts the EncryptedKey field with GCP KMS and returns
+// the result.
+func (key *MasterKey) Decrypt() ([]byte, error) {
+ service, err := key.newKMSClient()
+ if err != nil {
+ log.WithField("resourceID", key.ResourceID).Info("Decryption failed")
+ return nil, fmt.Errorf("cannot create GCP KMS service: %w", err)
+ }
+ defer func() {
+ if err := service.Close(); err != nil {
+ log.Error("failed to close GCP KMS client connection")
+ }
+ }()
+
+ // NB: this is for compatibility with SOPS <=3.8.x. The previous GCP KMS
+ // client used to work with base64 encoded strings.
+ decodedCipher, err := base64.StdEncoding.DecodeString(string(key.EncryptedDataKey()))
+ if err != nil {
+ log.WithField("resourceID", key.ResourceID).Info("Decryption failed")
+ return nil, err
+ }
+
+ req := &kmspb.DecryptRequest{
+ Name: key.ResourceID,
+ Ciphertext: decodedCipher,
+ }
+ ctx := context.Background()
+ resp, err := service.Decrypt(ctx, req)
+ if err != nil {
+ log.WithField("resourceID", key.ResourceID).Info("Decryption failed")
+ return nil, fmt.Errorf("failed to decrypt sops data key with GCP KMS key: %w", err)
+ }
+
+ log.WithField("resourceID", key.ResourceID).Info("Decryption succeeded")
+ return resp.Plaintext, nil
+}
+
+// NeedsRotation returns whether the data key needs to be rotated or not.
+func (key *MasterKey) NeedsRotation() bool {
+ return time.Since(key.CreationDate) > (gcpkmsTTL)
+}
+
+// ToString converts the key to a string representation.
+func (key *MasterKey) ToString() string {
+ return key.ResourceID
+}
+
+// ToMap converts the MasterKey to a map for serialization purposes.
+func (key MasterKey) ToMap() map[string]interface{} {
+ out := make(map[string]interface{})
+ out["resource_id"] = key.ResourceID
+ out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
+ out["enc"] = key.EncryptedKey
+ return out
+}
+
+// TypeToIdentifier returns the string identifier for the MasterKey type.
+func (key *MasterKey) TypeToIdentifier() string {
+ return KeyTypeIdentifier
+}
+
+// newKMSClient returns a GCP KMS client configured with the credentialJSON
+// and/or grpcConn, falling back to environmental defaults.
+// It returns an error if the ResourceID is invalid, or if the setup of the
+// client fails.
+func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
+ re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$`)
+ matches := re.FindStringSubmatch(key.ResourceID)
+ if matches == nil {
+ return nil, fmt.Errorf("no valid resource ID found in %q", key.ResourceID)
+ }
+
+ var opts []option.ClientOption
+ switch {
+ case key.credentialJSON != nil:
+ opts = append(opts, option.WithCredentialsJSON(key.credentialJSON))
+ default:
+ credentials, err := getGoogleCredentials()
+ if credentials != nil {
+ opts = append(opts, option.WithCredentialsJSON(credentials))
+ }
+
+ at_credentials, at_err := getGoogleAccessToken()
+ if at_credentials != nil {
+ opts = append(opts, option.WithTokenSource(at_credentials))
+ }
+
+ if err != nil && at_err != nil {
+ return nil, err
+ }
+ }
+ if key.grpcConn != nil {
+ opts = append(opts, option.WithGRPCConn(key.grpcConn))
+ }
+
+ ctx := context.Background()
+ client, err := kms.NewKeyManagementClient(ctx, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ return client, nil
+}
+
+// getGoogleCredentials returns the SopsGoogleCredentialsEnv variable, as
+// either the file contents of the path of a credentials file, or as value in
+// JSON format. It returns an error if the file cannot be read, and may return
+// a nil byte slice if no value is set.
+func getGoogleCredentials() ([]byte, error) {
+ if defaultCredentials, ok := os.LookupEnv(SopsGoogleCredentialsEnv); ok && len(defaultCredentials) > 0 {
+ if _, err := os.Stat(defaultCredentials); err == nil {
+ return os.ReadFile(defaultCredentials)
+ }
+ return []byte(defaultCredentials), nil
+ }
+ return nil, nil
+}
+
+func getGoogleAccessToken() (oauth2.TokenSource, error) {
+ if envToken, isSet := os.LookupEnv(SopsGoogleCredentialsAccessToken); isSet {
+ token := []byte(envToken)
+ if _, err := os.Stat(envToken); err == nil {
+ if token, err = os.ReadFile(envToken); err != nil {
+ return nil, err
+ }
+ }
+ tokenSource := oauth2.StaticTokenSource(
+ &oauth2.Token{AccessToken: string(token)},
+ )
+ return tokenSource, nil
+ }
+
+ return nil, nil
+}
diff --git a/gcpkms/gcpkms/keysource_test.go b/gcpkms/gcpkms/keysource_test.go
new file mode 100644
index 000000000..153bfb260
--- /dev/null
+++ b/gcpkms/gcpkms/keysource_test.go
@@ -0,0 +1,168 @@
+package gcpkms
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net"
+ "testing"
+ "time"
+
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+var (
+ testResourceID = "projects/test-sops/locations/global/keyRings/test-sops/cryptoKeys/sops"
+ decryptedData = "decrypted data"
+ encryptedData = "encrypted data"
+)
+
+var (
+ mockKeyManagement mockKeyManagementServer
+)
+
+func TestMasterKeysFromResourceIDString(t *testing.T) {
+ s := "projects/sops-testing1/locations/global/keyRings/creds/cryptoKeys/key1, projects/sops-testing2/locations/global/keyRings/creds/cryptoKeys/key2"
+ ks := MasterKeysFromResourceIDString(s)
+ k1 := ks[0]
+ k2 := ks[1]
+ expectedResourceID1 := "projects/sops-testing1/locations/global/keyRings/creds/cryptoKeys/key1"
+ expectedResourceID2 := "projects/sops-testing2/locations/global/keyRings/creds/cryptoKeys/key2"
+ if k1.ResourceID != expectedResourceID1 {
+ t.Errorf("ResourceID mismatch. Expected %s, found %s", expectedResourceID1, k1.ResourceID)
+ }
+ if k2.ResourceID != expectedResourceID2 {
+ t.Errorf("ResourceID mismatch. Expected %s, found %s", expectedResourceID2, k2.ResourceID)
+ }
+}
+
+func TestCredentialJSON_ApplyToMasterKey(t *testing.T) {
+ key := &MasterKey{}
+ credential := CredentialJSON("mock")
+ credential.ApplyToMasterKey(key)
+ assert.EqualValues(t, credential, key.credentialJSON)
+}
+
+func TestMasterKey_Encrypt(t *testing.T) {
+ mockKeyManagement.err = nil
+ mockKeyManagement.reqs = nil
+ mockKeyManagement.resps = append(mockKeyManagement.resps[:0], &kmspb.EncryptResponse{
+ Ciphertext: []byte(encryptedData),
+ })
+
+ key := MasterKey{
+ grpcConn: newGRPCServer("0"),
+ ResourceID: testResourceID,
+ }
+ err := key.Encrypt([]byte("encrypt"))
+ assert.NoError(t, err)
+ assert.EqualValues(t, base64.StdEncoding.EncodeToString([]byte(encryptedData)), key.EncryptedDataKey())
+}
+
+func TestMasterKey_EncryptIfNeeded(t *testing.T) {
+ key := MasterKey{EncryptedKey: encryptedData}
+ assert.EqualValues(t, encryptedData, key.EncryptedDataKey())
+ assert.NoError(t, key.EncryptIfNeeded([]byte("sops data key")))
+ assert.EqualValues(t, encryptedData, key.EncryptedDataKey())
+}
+
+func TestMasterKey_EncryptedDataKey(t *testing.T) {
+ key := MasterKey{EncryptedKey: encryptedData}
+ assert.EqualValues(t, encryptedData, key.EncryptedDataKey())
+}
+
+func TestMasterKey_Decrypt(t *testing.T) {
+ mockKeyManagement.err = nil
+ mockKeyManagement.reqs = nil
+ mockKeyManagement.resps = append(mockKeyManagement.resps[:0], &kmspb.DecryptResponse{
+ Plaintext: []byte(decryptedData),
+ })
+ key := MasterKey{
+ grpcConn: newGRPCServer("0"),
+ ResourceID: testResourceID,
+ EncryptedKey: "encryptedKey",
+ }
+ data, err := key.Decrypt()
+ assert.NoError(t, err)
+ assert.EqualValues(t, decryptedData, data)
+}
+
+func TestMasterKey_SetEncryptedDataKey(t *testing.T) {
+ enc := "encrypted key"
+ key := &MasterKey{}
+ key.SetEncryptedDataKey([]byte(enc))
+ assert.EqualValues(t, enc, key.EncryptedDataKey())
+}
+
+func TestMasterKey_ToString(t *testing.T) {
+ rsrcId := testResourceID
+ key := NewMasterKeyFromResourceID(rsrcId)
+ assert.Equal(t, rsrcId, key.ToString())
+}
+
+func TestMasterKey_ToMap(t *testing.T) {
+ key := MasterKey{
+ credentialJSON: []byte("sensitive creds"),
+ CreationDate: time.Date(2016, time.October, 31, 10, 0, 0, 0, time.UTC),
+ ResourceID: testResourceID,
+ EncryptedKey: "this is encrypted",
+ }
+ assert.Equal(t, map[string]interface{}{
+ "resource_id": testResourceID,
+ "enc": "this is encrypted",
+ "created_at": "2016-10-31T10:00:00Z",
+ }, key.ToMap())
+}
+
+func TestMasterKey_createCloudKMSService(t *testing.T) {
+ tests := []struct {
+ key MasterKey
+ errString string
+ }{
+ {
+ key: MasterKey{
+ ResourceID: "/projects",
+ credentialJSON: []byte("some secret"),
+ },
+ errString: "no valid resource ID",
+ },
+ {
+ key: MasterKey{
+ ResourceID: testResourceID,
+ credentialJSON: []byte(`{ "client_id": ".apps.googleusercontent.com",
+ "client_secret": "",
+ "type": "authorized_user"}`),
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ _, err := tt.key.newKMSClient()
+ if tt.errString != "" {
+ assert.Error(t, err)
+ assert.ErrorContains(t, err, tt.errString)
+ return
+ }
+ assert.NoError(t, err)
+ }
+}
+
+func newGRPCServer(port string) *grpc.ClientConn {
+ serv := grpc.NewServer()
+ kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement)
+
+ lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%s", port))
+ if err != nil {
+ log.Fatal(err)
+ }
+ go serv.Serve(lis)
+
+ conn, err := grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return conn
+}
diff --git a/gcpkms/gcpkms/mock_kms_server_test.go b/gcpkms/gcpkms/mock_kms_server_test.go
new file mode 100644
index 000000000..bd2f30d60
--- /dev/null
+++ b/gcpkms/gcpkms/mock_kms_server_test.go
@@ -0,0 +1,326 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code originally generated by gapic-generator.
+// Ref: https://github.com/googleapis/google-cloud-go/blob/4fe86a327f97ada275ce1744459129df38f9c95b/kms/apiv1/mock_test.go
+
+package gcpkms
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+
+ "cloud.google.com/go/kms/apiv1/kmspb"
+ "google.golang.org/genproto/googleapis/rpc/status"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/anypb"
+)
+
+var _ = io.EOF
+var _ = anypb.New
+var _ status.Status
+
+type mockKeyManagementServer struct {
+ // Embed for forward compatibility.
+ // Tests will keep working if more methods are added
+ // in the future.
+ kmspb.KeyManagementServiceServer
+
+ reqs []proto.Message
+
+ // If set, all calls return this error.
+ err error
+
+ // responses to return if err == nil
+ resps []proto.Message
+}
+
+func (s *mockKeyManagementServer) ListKeyRings(ctx context.Context, req *kmspb.ListKeyRingsRequest) (*kmspb.ListKeyRingsResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.ListKeyRingsResponse), nil
+}
+
+func (s *mockKeyManagementServer) ListCryptoKeys(ctx context.Context, req *kmspb.ListCryptoKeysRequest) (*kmspb.ListCryptoKeysResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.ListCryptoKeysResponse), nil
+}
+
+func (s *mockKeyManagementServer) ListCryptoKeyVersions(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest) (*kmspb.ListCryptoKeyVersionsResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.ListCryptoKeyVersionsResponse), nil
+}
+
+func (s *mockKeyManagementServer) ListImportJobs(ctx context.Context, req *kmspb.ListImportJobsRequest) (*kmspb.ListImportJobsResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.ListImportJobsResponse), nil
+}
+
+func (s *mockKeyManagementServer) GetKeyRing(ctx context.Context, req *kmspb.GetKeyRingRequest) (*kmspb.KeyRing, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.KeyRing), nil
+}
+
+func (s *mockKeyManagementServer) GetCryptoKey(ctx context.Context, req *kmspb.GetCryptoKeyRequest) (*kmspb.CryptoKey, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKey), nil
+}
+
+func (s *mockKeyManagementServer) GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKeyVersion), nil
+}
+
+func (s *mockKeyManagementServer) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest) (*kmspb.PublicKey, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.PublicKey), nil
+}
+
+func (s *mockKeyManagementServer) GetImportJob(ctx context.Context, req *kmspb.GetImportJobRequest) (*kmspb.ImportJob, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.ImportJob), nil
+}
+
+func (s *mockKeyManagementServer) CreateKeyRing(ctx context.Context, req *kmspb.CreateKeyRingRequest) (*kmspb.KeyRing, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.KeyRing), nil
+}
+
+func (s *mockKeyManagementServer) CreateCryptoKey(ctx context.Context, req *kmspb.CreateCryptoKeyRequest) (*kmspb.CryptoKey, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKey), nil
+}
+
+func (s *mockKeyManagementServer) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKeyVersion), nil
+}
+
+func (s *mockKeyManagementServer) ImportCryptoKeyVersion(ctx context.Context, req *kmspb.ImportCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKeyVersion), nil
+}
+
+func (s *mockKeyManagementServer) CreateImportJob(ctx context.Context, req *kmspb.CreateImportJobRequest) (*kmspb.ImportJob, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.ImportJob), nil
+}
+
+func (s *mockKeyManagementServer) UpdateCryptoKey(ctx context.Context, req *kmspb.UpdateCryptoKeyRequest) (*kmspb.CryptoKey, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKey), nil
+}
+
+func (s *mockKeyManagementServer) UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKeyVersion), nil
+}
+
+func (s *mockKeyManagementServer) Encrypt(ctx context.Context, req *kmspb.EncryptRequest) (*kmspb.EncryptResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.EncryptResponse), nil
+}
+
+func (s *mockKeyManagementServer) Decrypt(ctx context.Context, req *kmspb.DecryptRequest) (*kmspb.DecryptResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.DecryptResponse), nil
+}
+
+func (s *mockKeyManagementServer) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest) (*kmspb.AsymmetricSignResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.AsymmetricSignResponse), nil
+}
+
+func (s *mockKeyManagementServer) AsymmetricDecrypt(ctx context.Context, req *kmspb.AsymmetricDecryptRequest) (*kmspb.AsymmetricDecryptResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.AsymmetricDecryptResponse), nil
+}
+
+func (s *mockKeyManagementServer) UpdateCryptoKeyPrimaryVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyPrimaryVersionRequest) (*kmspb.CryptoKey, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKey), nil
+}
+
+func (s *mockKeyManagementServer) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.DestroyCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKeyVersion), nil
+}
+
+func (s *mockKeyManagementServer) RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") {
+ return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg)
+ }
+ s.reqs = append(s.reqs, req)
+ if s.err != nil {
+ return nil, s.err
+ }
+ return s.resps[0].(*kmspb.CryptoKeyVersion), nil
+}
diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go
index 733f53c20..903db5254 100644
--- a/gcpkms/keysource.go
+++ b/gcpkms/keysource.go
@@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"fmt"
- "golang.org/x/oauth2"
"os"
"regexp"
"strings"
@@ -13,6 +12,7 @@ import (
kms "cloud.google.com/go/kms/apiv1"
"cloud.google.com/go/kms/apiv1/kmspb"
"github.com/sirupsen/logrus"
+ "golang.org/x/oauth2"
"google.golang.org/api/option"
"google.golang.org/grpc"
@@ -23,12 +23,8 @@ const (
// SopsGoogleCredentialsEnv can be set as an environment variable as either
// a path to a credentials file, or directly as the variable's value in JSON
// format.
- SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS"
-
- // SopsGoogleAccessTokenEnv can be set as an environment variable as
- // directly the google access token, instead of a credentials file
- SopsGoogleAccessTokenEnv = "CLOUDSDK_AUTH_ACCESS_TOKEN"
-
+ SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS"
+ SopsGoogleCredentialsAccessToken = "CLOUDSDK_AUTH_ACCESS_TOKEN"
// KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey.
KeyTypeIdentifier = "gcp_kms"
)
@@ -59,8 +55,6 @@ type MasterKey struct {
// credentialJSON is the Service Account credentials JSON used for
// authenticating towards the GCP KMS service.
credentialJSON []byte
-
- AccessToken AccessToken
// grpcConn can be used to inject a custom GCP client connection.
// Mostly useful for testing at present, to wire the client to a mock
// server.
@@ -90,12 +84,6 @@ func MasterKeysFromResourceIDString(resourceID string) []*MasterKey {
return keys
}
-type AccessToken string
-
-func (a AccessToken) ApplyToMasterKey(key *MasterKey) {
- key.AccessToken = a
-}
-
// CredentialJSON is the Service Account credentials JSON used for authenticating
// towards the GCP KMS service.
type CredentialJSON []byte
@@ -230,19 +218,21 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
var opts []option.ClientOption
switch {
- case key.AccessToken != "":
- opts = append(opts, option.WithTokenSource(oauth2.StaticTokenSource(
- &oauth2.Token{AccessToken: string(key.AccessToken)},
- )))
case key.credentialJSON != nil:
opts = append(opts, option.WithCredentialsJSON(key.credentialJSON))
default:
- credentialsOption, err := getGoogleCredentials()
- if err != nil {
- return nil, err
+ credentials, err := getGoogleCredentials()
+ if credentials != nil {
+ opts = append(opts, option.WithCredentialsJSON(credentials))
}
- if credentialsOption != nil {
- opts = append(opts, credentialsOption)
+
+ at_credentials, at_err := getGoogleAccessToken()
+ if at_credentials != nil {
+ opts = append(opts, option.WithTokenSource(at_credentials))
+ }
+
+ if err != nil && at_err != nil {
+ return nil, err
}
}
if key.grpcConn != nil {
@@ -262,17 +252,18 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) {
// either the file contents of the path of a credentials file, or as value in
// JSON format. It returns an error if the file cannot be read, and may return
// a nil byte slice if no value is set.
-func getGoogleCredentials() (option.ClientOption, error) {
- if envCredentials, isSet := os.LookupEnv(SopsGoogleCredentialsEnv); isSet {
- credentials := []byte(envCredentials)
- if _, err := os.Stat(envCredentials); err == nil {
- if credentials, err = os.ReadFile(envCredentials); err != nil {
- return nil, err
- }
+func getGoogleCredentials() ([]byte, error) {
+ if defaultCredentials, ok := os.LookupEnv(SopsGoogleCredentialsEnv); ok && len(defaultCredentials) > 0 {
+ if _, err := os.Stat(defaultCredentials); err == nil {
+ return os.ReadFile(defaultCredentials)
}
- return option.WithCredentialsJSON(credentials), nil
+ return []byte(defaultCredentials), nil
}
- if envToken, isSet := os.LookupEnv(SopsGoogleAccessTokenEnv); isSet {
+ return nil, nil
+}
+
+func getGoogleAccessToken() (oauth2.TokenSource, error) {
+ if envToken, isSet := os.LookupEnv(SopsGoogleCredentialsAccessToken); isSet {
token := []byte(envToken)
if _, err := os.Stat(envToken); err == nil {
if token, err = os.ReadFile(envToken); err != nil {
@@ -282,7 +273,8 @@ func getGoogleCredentials() (option.ClientOption, error) {
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: string(token)},
)
- return option.WithTokenSource(tokenSource), nil
+ return tokenSource, nil
}
+
return nil, nil
}
diff --git a/hcvault/keysource.go b/hcvault/keysource.go
index a025413fe..c120d9c99 100644
--- a/hcvault/keysource.go
+++ b/hcvault/keysource.go
@@ -317,7 +317,7 @@ func vaultClient(address, token string) (*api.Client, error) {
return client, nil
}
-// userVaultsToken returns the token from `$HOME/.vault-token` if the file
+// userVaultToken returns the token from `$HOME/.vault-token` if the file
// exists. It returns an error if the file exists but cannot be read from.
// If the file does not exist, it returns an empty string.
func userVaultToken() (string, error) {
diff --git a/keyservice/client.go b/keyservice/client.go
index 6ba02e64f..f0a29fd06 100644
--- a/keyservice/client.go
+++ b/keyservice/client.go
@@ -13,7 +13,7 @@ type LocalClient struct {
// NewLocalClient creates a new local client
func NewLocalClient() LocalClient {
- return LocalClient{&Server{}}
+ return LocalClient{Server{}}
}
// NewCustomLocalClient creates a new local client with a non-default backing
diff --git a/keyservice/keyservice.go b/keyservice/keyservice.go
index 2886a5d6a..321af7942 100644
--- a/keyservice/keyservice.go
+++ b/keyservice/keyservice.go
@@ -17,7 +17,7 @@ import (
)
// KeyFromMasterKey converts a SOPS internal MasterKey to an RPC Key that can be serialized with Protocol Buffers
-func KeyFromMasterKey(mk keys.MasterKey, credentials map[string]string) Key {
+func KeyFromMasterKey(mk keys.MasterKey) Key {
switch mk := mk.(type) {
case *pgp.MasterKey:
return Key{
@@ -31,8 +31,7 @@ func KeyFromMasterKey(mk keys.MasterKey, credentials map[string]string) Key {
return Key{
KeyType: &Key_GcpKmsKey{
GcpKmsKey: &GcpKmsKey{
- ResourceId: mk.ResourceID,
- AccessToken: credentials["gcp-kms"],
+ ResourceId: mk.ResourceID,
},
},
}
diff --git a/keyservice/keyservice.pb.go b/keyservice/keyservice.pb.go
index 4ef24ff9e..ead3ccfd1 100644
--- a/keyservice/keyservice.pb.go
+++ b/keyservice/keyservice.pb.go
@@ -1,12 +1,17 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.32.0
-// protoc v4.25.3
+// protoc-gen-go v1.23.0
+// protoc v3.13.0
// source: keyservice/keyservice.proto
package keyservice
import (
+ context "context"
+ proto "github.com/golang/protobuf/proto"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -20,13 +25,16 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
type Key struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to KeyType:
- //
// *Key_KmsKey
// *Key_PgpKey
// *Key_GcpKmsKey
@@ -280,8 +288,7 @@ type GcpKmsKey struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
- AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
+ ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
}
func (x *GcpKmsKey) Reset() {
@@ -323,13 +330,6 @@ func (x *GcpKmsKey) GetResourceId() string {
return ""
}
-func (x *GcpKmsKey) GetAccessToken() string {
- if x != nil {
- return x.AccessToken
- }
- return ""
-}
-
type VaultKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -744,51 +744,47 @@ var file_keyservice_keyservice_proto_rawDesc = []byte{
0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
- 0x38, 0x01, 0x22, 0x4f, 0x0a, 0x09, 0x47, 0x63, 0x70, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12,
+ 0x38, 0x01, 0x22, 0x2c, 0x0a, 0x09, 0x47, 0x63, 0x70, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12,
0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64,
- 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f,
- 0x6b, 0x65, 0x6e, 0x22, 0x6b, 0x0a, 0x08, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12,
- 0x23, 0x0a, 0x0d, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x64,
- 0x72, 0x65, 0x73, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5f, 0x70,
- 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x67, 0x69, 0x6e,
- 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6e, 0x61, 0x6d,
- 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65,
- 0x22, 0x5d, 0x0a, 0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, 0x6c,
- 0x74, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x75, 0x72,
- 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x72,
- 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22,
- 0x26, 0x0a, 0x06, 0x41, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63,
- 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65,
- 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79,
- 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65,
- 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22,
- 0x31, 0x0a, 0x0f, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65,
- 0x78, 0x74, 0x22, 0x48, 0x0a, 0x0e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x0b, 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a,
- 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
- 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x2f, 0x0a, 0x0f,
- 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x1c, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x32, 0x6c, 0x0a,
- 0x0a, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x45,
- 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
- 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x07, 0x44,
- 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70,
- 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0d, 0x5a, 0x0b, 0x6b,
- 0x65, 0x79, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x33,
+ 0x22, 0x6b, 0x0a, 0x08, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d,
+ 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0c, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
+ 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x50, 0x61,
+ 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x5d, 0x0a,
+ 0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65,
+ 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x12,
+ 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
+ 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x26, 0x0a, 0x06,
+ 0x41, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69,
+ 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70,
+ 0x69, 0x65, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1c,
+ 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x31, 0x0a, 0x0f,
+ 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22,
+ 0x48, 0x0a, 0x0e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x04,
+ 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70,
+ 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63,
+ 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x2f, 0x0a, 0x0f, 0x44, 0x65, 0x63,
+ 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09,
+ 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x32, 0x6c, 0x0a, 0x0a, 0x4b, 0x65,
+ 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x45, 0x6e, 0x63, 0x72,
+ 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x07, 0x44, 0x65, 0x63, 0x72,
+ 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1005,3 +1001,119 @@ func file_keyservice_keyservice_proto_init() {
file_keyservice_keyservice_proto_goTypes = nil
file_keyservice_keyservice_proto_depIdxs = nil
}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// KeyServiceClient is the client API for KeyService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type KeyServiceClient interface {
+ Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error)
+ Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error)
+}
+
+type keyServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewKeyServiceClient(cc grpc.ClientConnInterface) KeyServiceClient {
+ return &keyServiceClient{cc}
+}
+
+func (c *keyServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) {
+ out := new(EncryptResponse)
+ err := c.cc.Invoke(ctx, "/KeyService/Encrypt", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *keyServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) {
+ out := new(DecryptResponse)
+ err := c.cc.Invoke(ctx, "/KeyService/Decrypt", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// KeyServiceServer is the server API for KeyService service.
+type KeyServiceServer interface {
+ Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error)
+ Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error)
+}
+
+// UnimplementedKeyServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedKeyServiceServer struct {
+}
+
+func (*UnimplementedKeyServiceServer) Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Encrypt not implemented")
+}
+func (*UnimplementedKeyServiceServer) Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Decrypt not implemented")
+}
+
+func RegisterKeyServiceServer(s *grpc.Server, srv KeyServiceServer) {
+ s.RegisterService(&_KeyService_serviceDesc, srv)
+}
+
+func _KeyService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(EncryptRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(KeyServiceServer).Encrypt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/KeyService/Encrypt",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(KeyServiceServer).Encrypt(ctx, req.(*EncryptRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _KeyService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DecryptRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(KeyServiceServer).Decrypt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/KeyService/Decrypt",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(KeyServiceServer).Decrypt(ctx, req.(*DecryptRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _KeyService_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "KeyService",
+ HandlerType: (*KeyServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Encrypt",
+ Handler: _KeyService_Encrypt_Handler,
+ },
+ {
+ MethodName: "Decrypt",
+ Handler: _KeyService_Decrypt_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "keyservice/keyservice.proto",
+}
diff --git a/keyservice/keyservice.proto b/keyservice/keyservice.proto
index 0ad7dd580..1d91a5709 100644
--- a/keyservice/keyservice.proto
+++ b/keyservice/keyservice.proto
@@ -1,7 +1,5 @@
syntax = "proto3";
-option go_package="keyservice/";
-
message Key {
oneof key_type {
KmsKey kms_key = 1;
@@ -26,7 +24,6 @@ message KmsKey {
message GcpKmsKey {
string resource_id = 1;
- string access_token = 2;
}
message VaultKey {
diff --git a/keyservice/keyservice/client.go b/keyservice/keyservice/client.go
new file mode 100644
index 000000000..f0a29fd06
--- /dev/null
+++ b/keyservice/keyservice/client.go
@@ -0,0 +1,37 @@
+package keyservice
+
+import (
+ "golang.org/x/net/context"
+
+ "google.golang.org/grpc"
+)
+
+// LocalClient is a key service client that performs all operations locally
+type LocalClient struct {
+ Server KeyServiceServer
+}
+
+// NewLocalClient creates a new local client
+func NewLocalClient() LocalClient {
+ return LocalClient{Server{}}
+}
+
+// NewCustomLocalClient creates a new local client with a non-default backing
+// KeyServiceServer implementation
+func NewCustomLocalClient(server KeyServiceServer) LocalClient {
+ return LocalClient{Server: server}
+}
+
+// Decrypt processes a decrypt request locally
+// See keyservice/server.go for more details
+func (c LocalClient) Decrypt(ctx context.Context,
+ req *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) {
+ return c.Server.Decrypt(ctx, req)
+}
+
+// Encrypt processes an encrypt request locally
+// See keyservice/server.go for more details
+func (c LocalClient) Encrypt(ctx context.Context,
+ req *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) {
+ return c.Server.Encrypt(ctx, req)
+}
diff --git a/keyservice/keyservice/keyservice.go b/keyservice/keyservice/keyservice.go
new file mode 100644
index 000000000..321af7942
--- /dev/null
+++ b/keyservice/keyservice/keyservice.go
@@ -0,0 +1,84 @@
+/*
+Package keyservice implements a gRPC API that can be used by SOPS to encrypt and decrypt the data key using remote
+master keys.
+*/
+package keyservice
+
+import (
+ "fmt"
+
+ "github.com/getsops/sops/v3/age"
+ "github.com/getsops/sops/v3/azkv"
+ "github.com/getsops/sops/v3/gcpkms"
+ "github.com/getsops/sops/v3/hcvault"
+ "github.com/getsops/sops/v3/keys"
+ "github.com/getsops/sops/v3/kms"
+ "github.com/getsops/sops/v3/pgp"
+)
+
+// KeyFromMasterKey converts a SOPS internal MasterKey to an RPC Key that can be serialized with Protocol Buffers
+func KeyFromMasterKey(mk keys.MasterKey) Key {
+ switch mk := mk.(type) {
+ case *pgp.MasterKey:
+ return Key{
+ KeyType: &Key_PgpKey{
+ PgpKey: &PgpKey{
+ Fingerprint: mk.Fingerprint,
+ },
+ },
+ }
+ case *gcpkms.MasterKey:
+ return Key{
+ KeyType: &Key_GcpKmsKey{
+ GcpKmsKey: &GcpKmsKey{
+ ResourceId: mk.ResourceID,
+ },
+ },
+ }
+ case *hcvault.MasterKey:
+ return Key{
+ KeyType: &Key_VaultKey{
+ VaultKey: &VaultKey{
+ VaultAddress: mk.VaultAddress,
+ EnginePath: mk.EnginePath,
+ KeyName: mk.KeyName,
+ },
+ },
+ }
+ case *kms.MasterKey:
+ ctx := make(map[string]string)
+ for k, v := range mk.EncryptionContext {
+ ctx[k] = *v
+ }
+ return Key{
+ KeyType: &Key_KmsKey{
+ KmsKey: &KmsKey{
+ Arn: mk.Arn,
+ Role: mk.Role,
+ Context: ctx,
+ AwsProfile: mk.AwsProfile,
+ },
+ },
+ }
+ case *azkv.MasterKey:
+ return Key{
+ KeyType: &Key_AzureKeyvaultKey{
+ AzureKeyvaultKey: &AzureKeyVaultKey{
+ VaultUrl: mk.VaultURL,
+ Name: mk.Name,
+ Version: mk.Version,
+ },
+ },
+ }
+ case *age.MasterKey:
+ return Key{
+ KeyType: &Key_AgeKey{
+ AgeKey: &AgeKey{
+ Recipient: mk.Recipient,
+ },
+ },
+ }
+ default:
+ panic(fmt.Sprintf("Tried to convert unknown MasterKey type %T to keyservice.Key", mk))
+ }
+}
diff --git a/keyservice/keyservice/keyservice.pb.go b/keyservice/keyservice/keyservice.pb.go
new file mode 100644
index 000000000..ead3ccfd1
--- /dev/null
+++ b/keyservice/keyservice/keyservice.pb.go
@@ -0,0 +1,1119 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.23.0
+// protoc v3.13.0
+// source: keyservice/keyservice.proto
+
+package keyservice
+
+import (
+ context "context"
+ proto "github.com/golang/protobuf/proto"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
+type Key struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // Types that are assignable to KeyType:
+ // *Key_KmsKey
+ // *Key_PgpKey
+ // *Key_GcpKmsKey
+ // *Key_AzureKeyvaultKey
+ // *Key_VaultKey
+ // *Key_AgeKey
+ KeyType isKey_KeyType `protobuf_oneof:"key_type"`
+}
+
+func (x *Key) Reset() {
+ *x = Key{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Key) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Key) ProtoMessage() {}
+
+func (x *Key) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Key.ProtoReflect.Descriptor instead.
+func (*Key) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{0}
+}
+
+func (m *Key) GetKeyType() isKey_KeyType {
+ if m != nil {
+ return m.KeyType
+ }
+ return nil
+}
+
+func (x *Key) GetKmsKey() *KmsKey {
+ if x, ok := x.GetKeyType().(*Key_KmsKey); ok {
+ return x.KmsKey
+ }
+ return nil
+}
+
+func (x *Key) GetPgpKey() *PgpKey {
+ if x, ok := x.GetKeyType().(*Key_PgpKey); ok {
+ return x.PgpKey
+ }
+ return nil
+}
+
+func (x *Key) GetGcpKmsKey() *GcpKmsKey {
+ if x, ok := x.GetKeyType().(*Key_GcpKmsKey); ok {
+ return x.GcpKmsKey
+ }
+ return nil
+}
+
+func (x *Key) GetAzureKeyvaultKey() *AzureKeyVaultKey {
+ if x, ok := x.GetKeyType().(*Key_AzureKeyvaultKey); ok {
+ return x.AzureKeyvaultKey
+ }
+ return nil
+}
+
+func (x *Key) GetVaultKey() *VaultKey {
+ if x, ok := x.GetKeyType().(*Key_VaultKey); ok {
+ return x.VaultKey
+ }
+ return nil
+}
+
+func (x *Key) GetAgeKey() *AgeKey {
+ if x, ok := x.GetKeyType().(*Key_AgeKey); ok {
+ return x.AgeKey
+ }
+ return nil
+}
+
+type isKey_KeyType interface {
+ isKey_KeyType()
+}
+
+type Key_KmsKey struct {
+ KmsKey *KmsKey `protobuf:"bytes,1,opt,name=kms_key,json=kmsKey,proto3,oneof"`
+}
+
+type Key_PgpKey struct {
+ PgpKey *PgpKey `protobuf:"bytes,2,opt,name=pgp_key,json=pgpKey,proto3,oneof"`
+}
+
+type Key_GcpKmsKey struct {
+ GcpKmsKey *GcpKmsKey `protobuf:"bytes,3,opt,name=gcp_kms_key,json=gcpKmsKey,proto3,oneof"`
+}
+
+type Key_AzureKeyvaultKey struct {
+ AzureKeyvaultKey *AzureKeyVaultKey `protobuf:"bytes,4,opt,name=azure_keyvault_key,json=azureKeyvaultKey,proto3,oneof"`
+}
+
+type Key_VaultKey struct {
+ VaultKey *VaultKey `protobuf:"bytes,5,opt,name=vault_key,json=vaultKey,proto3,oneof"`
+}
+
+type Key_AgeKey struct {
+ AgeKey *AgeKey `protobuf:"bytes,6,opt,name=age_key,json=ageKey,proto3,oneof"`
+}
+
+func (*Key_KmsKey) isKey_KeyType() {}
+
+func (*Key_PgpKey) isKey_KeyType() {}
+
+func (*Key_GcpKmsKey) isKey_KeyType() {}
+
+func (*Key_AzureKeyvaultKey) isKey_KeyType() {}
+
+func (*Key_VaultKey) isKey_KeyType() {}
+
+func (*Key_AgeKey) isKey_KeyType() {}
+
+type PgpKey struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Fingerprint string `protobuf:"bytes,1,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"`
+}
+
+func (x *PgpKey) Reset() {
+ *x = PgpKey{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *PgpKey) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PgpKey) ProtoMessage() {}
+
+func (x *PgpKey) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PgpKey.ProtoReflect.Descriptor instead.
+func (*PgpKey) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PgpKey) GetFingerprint() string {
+ if x != nil {
+ return x.Fingerprint
+ }
+ return ""
+}
+
+type KmsKey struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Arn string `protobuf:"bytes,1,opt,name=arn,proto3" json:"arn,omitempty"`
+ Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"`
+ Context map[string]string `protobuf:"bytes,3,rep,name=context,proto3" json:"context,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+ AwsProfile string `protobuf:"bytes,4,opt,name=aws_profile,json=awsProfile,proto3" json:"aws_profile,omitempty"`
+}
+
+func (x *KmsKey) Reset() {
+ *x = KmsKey{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *KmsKey) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KmsKey) ProtoMessage() {}
+
+func (x *KmsKey) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use KmsKey.ProtoReflect.Descriptor instead.
+func (*KmsKey) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *KmsKey) GetArn() string {
+ if x != nil {
+ return x.Arn
+ }
+ return ""
+}
+
+func (x *KmsKey) GetRole() string {
+ if x != nil {
+ return x.Role
+ }
+ return ""
+}
+
+func (x *KmsKey) GetContext() map[string]string {
+ if x != nil {
+ return x.Context
+ }
+ return nil
+}
+
+func (x *KmsKey) GetAwsProfile() string {
+ if x != nil {
+ return x.AwsProfile
+ }
+ return ""
+}
+
+type GcpKmsKey struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
+}
+
+func (x *GcpKmsKey) Reset() {
+ *x = GcpKmsKey{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GcpKmsKey) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GcpKmsKey) ProtoMessage() {}
+
+func (x *GcpKmsKey) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GcpKmsKey.ProtoReflect.Descriptor instead.
+func (*GcpKmsKey) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *GcpKmsKey) GetResourceId() string {
+ if x != nil {
+ return x.ResourceId
+ }
+ return ""
+}
+
+type VaultKey struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ VaultAddress string `protobuf:"bytes,1,opt,name=vault_address,json=vaultAddress,proto3" json:"vault_address,omitempty"`
+ EnginePath string `protobuf:"bytes,2,opt,name=engine_path,json=enginePath,proto3" json:"engine_path,omitempty"`
+ KeyName string `protobuf:"bytes,3,opt,name=key_name,json=keyName,proto3" json:"key_name,omitempty"`
+}
+
+func (x *VaultKey) Reset() {
+ *x = VaultKey{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *VaultKey) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VaultKey) ProtoMessage() {}
+
+func (x *VaultKey) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use VaultKey.ProtoReflect.Descriptor instead.
+func (*VaultKey) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *VaultKey) GetVaultAddress() string {
+ if x != nil {
+ return x.VaultAddress
+ }
+ return ""
+}
+
+func (x *VaultKey) GetEnginePath() string {
+ if x != nil {
+ return x.EnginePath
+ }
+ return ""
+}
+
+func (x *VaultKey) GetKeyName() string {
+ if x != nil {
+ return x.KeyName
+ }
+ return ""
+}
+
+type AzureKeyVaultKey struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ VaultUrl string `protobuf:"bytes,1,opt,name=vault_url,json=vaultUrl,proto3" json:"vault_url,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+ Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"`
+}
+
+func (x *AzureKeyVaultKey) Reset() {
+ *x = AzureKeyVaultKey{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AzureKeyVaultKey) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AzureKeyVaultKey) ProtoMessage() {}
+
+func (x *AzureKeyVaultKey) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AzureKeyVaultKey.ProtoReflect.Descriptor instead.
+func (*AzureKeyVaultKey) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *AzureKeyVaultKey) GetVaultUrl() string {
+ if x != nil {
+ return x.VaultUrl
+ }
+ return ""
+}
+
+func (x *AzureKeyVaultKey) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *AzureKeyVaultKey) GetVersion() string {
+ if x != nil {
+ return x.Version
+ }
+ return ""
+}
+
+type AgeKey struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Recipient string `protobuf:"bytes,1,opt,name=recipient,proto3" json:"recipient,omitempty"`
+}
+
+func (x *AgeKey) Reset() {
+ *x = AgeKey{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AgeKey) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AgeKey) ProtoMessage() {}
+
+func (x *AgeKey) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AgeKey.ProtoReflect.Descriptor instead.
+func (*AgeKey) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *AgeKey) GetRecipient() string {
+ if x != nil {
+ return x.Recipient
+ }
+ return ""
+}
+
+type EncryptRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+ Plaintext []byte `protobuf:"bytes,2,opt,name=plaintext,proto3" json:"plaintext,omitempty"`
+}
+
+func (x *EncryptRequest) Reset() {
+ *x = EncryptRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *EncryptRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EncryptRequest) ProtoMessage() {}
+
+func (x *EncryptRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use EncryptRequest.ProtoReflect.Descriptor instead.
+func (*EncryptRequest) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *EncryptRequest) GetKey() *Key {
+ if x != nil {
+ return x.Key
+ }
+ return nil
+}
+
+func (x *EncryptRequest) GetPlaintext() []byte {
+ if x != nil {
+ return x.Plaintext
+ }
+ return nil
+}
+
+type EncryptResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Ciphertext []byte `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"`
+}
+
+func (x *EncryptResponse) Reset() {
+ *x = EncryptResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *EncryptResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EncryptResponse) ProtoMessage() {}
+
+func (x *EncryptResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use EncryptResponse.ProtoReflect.Descriptor instead.
+func (*EncryptResponse) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *EncryptResponse) GetCiphertext() []byte {
+ if x != nil {
+ return x.Ciphertext
+ }
+ return nil
+}
+
+type DecryptRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+ Ciphertext []byte `protobuf:"bytes,2,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"`
+}
+
+func (x *DecryptRequest) Reset() {
+ *x = DecryptRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DecryptRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DecryptRequest) ProtoMessage() {}
+
+func (x *DecryptRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DecryptRequest.ProtoReflect.Descriptor instead.
+func (*DecryptRequest) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *DecryptRequest) GetKey() *Key {
+ if x != nil {
+ return x.Key
+ }
+ return nil
+}
+
+func (x *DecryptRequest) GetCiphertext() []byte {
+ if x != nil {
+ return x.Ciphertext
+ }
+ return nil
+}
+
+type DecryptResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Plaintext []byte `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"`
+}
+
+func (x *DecryptResponse) Reset() {
+ *x = DecryptResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_keyservice_keyservice_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DecryptResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DecryptResponse) ProtoMessage() {}
+
+func (x *DecryptResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_keyservice_keyservice_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DecryptResponse.ProtoReflect.Descriptor instead.
+func (*DecryptResponse) Descriptor() ([]byte, []int) {
+ return file_keyservice_keyservice_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *DecryptResponse) GetPlaintext() []byte {
+ if x != nil {
+ return x.Plaintext
+ }
+ return nil
+}
+
+var File_keyservice_keyservice_proto protoreflect.FileDescriptor
+
+var file_keyservice_keyservice_proto_rawDesc = []byte{
+ 0x0a, 0x1b, 0x6b, 0x65, 0x79, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x6b, 0x65, 0x79,
+ 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x98, 0x02,
+ 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, 0x6b, 0x6d, 0x73, 0x5f, 0x6b, 0x65, 0x79,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x48,
+ 0x00, 0x52, 0x06, 0x6b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, 0x70, 0x67, 0x70,
+ 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x50, 0x67, 0x70,
+ 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x06, 0x70, 0x67, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a,
+ 0x0b, 0x67, 0x63, 0x70, 0x5f, 0x6b, 0x6d, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x47, 0x63, 0x70, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x48, 0x00,
+ 0x52, 0x09, 0x67, 0x63, 0x70, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x12, 0x61,
+ 0x7a, 0x75, 0x72, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x6b, 0x65,
+ 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b,
+ 0x65, 0x79, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x10, 0x61, 0x7a,
+ 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x28,
+ 0x0a, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x09, 0x2e, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x08,
+ 0x76, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x07, 0x61, 0x67, 0x65, 0x5f,
+ 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x41, 0x67, 0x65, 0x4b,
+ 0x65, 0x79, 0x48, 0x00, 0x52, 0x06, 0x61, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08,
+ 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2a, 0x0a, 0x06, 0x50, 0x67, 0x70, 0x4b,
+ 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e,
+ 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70,
+ 0x72, 0x69, 0x6e, 0x74, 0x22, 0xbb, 0x01, 0x0a, 0x06, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12,
+ 0x10, 0x0a, 0x03, 0x61, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x72,
+ 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74,
+ 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x2e,
+ 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6f,
+ 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x77, 0x73, 0x5f, 0x70, 0x72, 0x6f,
+ 0x66, 0x69, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x77, 0x73, 0x50,
+ 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x1a, 0x3a, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78,
+ 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
+ 0x38, 0x01, 0x22, 0x2c, 0x0a, 0x09, 0x47, 0x63, 0x70, 0x4b, 0x6d, 0x73, 0x4b, 0x65, 0x79, 0x12,
+ 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64,
+ 0x22, 0x6b, 0x0a, 0x08, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d,
+ 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0c, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
+ 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x50, 0x61,
+ 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x5d, 0x0a,
+ 0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x75, 0x6c, 0x74, 0x4b, 0x65,
+ 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x12,
+ 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
+ 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x26, 0x0a, 0x06,
+ 0x41, 0x67, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69,
+ 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70,
+ 0x69, 0x65, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x0e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1c,
+ 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x0c, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x31, 0x0a, 0x0f,
+ 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22,
+ 0x48, 0x0a, 0x0e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x16, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x04,
+ 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x69, 0x70,
+ 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63,
+ 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x2f, 0x0a, 0x0f, 0x44, 0x65, 0x63,
+ 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09,
+ 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x09, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x32, 0x6c, 0x0a, 0x0a, 0x4b, 0x65,
+ 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x45, 0x6e, 0x63, 0x72,
+ 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x07, 0x44, 0x65, 0x63, 0x72,
+ 0x79, 0x70, 0x74, 0x12, 0x0f, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_keyservice_keyservice_proto_rawDescOnce sync.Once
+ file_keyservice_keyservice_proto_rawDescData = file_keyservice_keyservice_proto_rawDesc
+)
+
+func file_keyservice_keyservice_proto_rawDescGZIP() []byte {
+ file_keyservice_keyservice_proto_rawDescOnce.Do(func() {
+ file_keyservice_keyservice_proto_rawDescData = protoimpl.X.CompressGZIP(file_keyservice_keyservice_proto_rawDescData)
+ })
+ return file_keyservice_keyservice_proto_rawDescData
+}
+
+var file_keyservice_keyservice_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
+var file_keyservice_keyservice_proto_goTypes = []interface{}{
+ (*Key)(nil), // 0: Key
+ (*PgpKey)(nil), // 1: PgpKey
+ (*KmsKey)(nil), // 2: KmsKey
+ (*GcpKmsKey)(nil), // 3: GcpKmsKey
+ (*VaultKey)(nil), // 4: VaultKey
+ (*AzureKeyVaultKey)(nil), // 5: AzureKeyVaultKey
+ (*AgeKey)(nil), // 6: AgeKey
+ (*EncryptRequest)(nil), // 7: EncryptRequest
+ (*EncryptResponse)(nil), // 8: EncryptResponse
+ (*DecryptRequest)(nil), // 9: DecryptRequest
+ (*DecryptResponse)(nil), // 10: DecryptResponse
+ nil, // 11: KmsKey.ContextEntry
+}
+var file_keyservice_keyservice_proto_depIdxs = []int32{
+ 2, // 0: Key.kms_key:type_name -> KmsKey
+ 1, // 1: Key.pgp_key:type_name -> PgpKey
+ 3, // 2: Key.gcp_kms_key:type_name -> GcpKmsKey
+ 5, // 3: Key.azure_keyvault_key:type_name -> AzureKeyVaultKey
+ 4, // 4: Key.vault_key:type_name -> VaultKey
+ 6, // 5: Key.age_key:type_name -> AgeKey
+ 11, // 6: KmsKey.context:type_name -> KmsKey.ContextEntry
+ 0, // 7: EncryptRequest.key:type_name -> Key
+ 0, // 8: DecryptRequest.key:type_name -> Key
+ 7, // 9: KeyService.Encrypt:input_type -> EncryptRequest
+ 9, // 10: KeyService.Decrypt:input_type -> DecryptRequest
+ 8, // 11: KeyService.Encrypt:output_type -> EncryptResponse
+ 10, // 12: KeyService.Decrypt:output_type -> DecryptResponse
+ 11, // [11:13] is the sub-list for method output_type
+ 9, // [9:11] is the sub-list for method input_type
+ 9, // [9:9] is the sub-list for extension type_name
+ 9, // [9:9] is the sub-list for extension extendee
+ 0, // [0:9] is the sub-list for field type_name
+}
+
+func init() { file_keyservice_keyservice_proto_init() }
+func file_keyservice_keyservice_proto_init() {
+ if File_keyservice_keyservice_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_keyservice_keyservice_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Key); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PgpKey); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*KmsKey); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GcpKmsKey); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*VaultKey); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AzureKeyVaultKey); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AgeKey); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*EncryptRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*EncryptResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DecryptRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DecryptResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ file_keyservice_keyservice_proto_msgTypes[0].OneofWrappers = []interface{}{
+ (*Key_KmsKey)(nil),
+ (*Key_PgpKey)(nil),
+ (*Key_GcpKmsKey)(nil),
+ (*Key_AzureKeyvaultKey)(nil),
+ (*Key_VaultKey)(nil),
+ (*Key_AgeKey)(nil),
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_keyservice_keyservice_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 12,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_keyservice_keyservice_proto_goTypes,
+ DependencyIndexes: file_keyservice_keyservice_proto_depIdxs,
+ MessageInfos: file_keyservice_keyservice_proto_msgTypes,
+ }.Build()
+ File_keyservice_keyservice_proto = out.File
+ file_keyservice_keyservice_proto_rawDesc = nil
+ file_keyservice_keyservice_proto_goTypes = nil
+ file_keyservice_keyservice_proto_depIdxs = nil
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// KeyServiceClient is the client API for KeyService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type KeyServiceClient interface {
+ Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error)
+ Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error)
+}
+
+type keyServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewKeyServiceClient(cc grpc.ClientConnInterface) KeyServiceClient {
+ return &keyServiceClient{cc}
+}
+
+func (c *keyServiceClient) Encrypt(ctx context.Context, in *EncryptRequest, opts ...grpc.CallOption) (*EncryptResponse, error) {
+ out := new(EncryptResponse)
+ err := c.cc.Invoke(ctx, "/KeyService/Encrypt", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *keyServiceClient) Decrypt(ctx context.Context, in *DecryptRequest, opts ...grpc.CallOption) (*DecryptResponse, error) {
+ out := new(DecryptResponse)
+ err := c.cc.Invoke(ctx, "/KeyService/Decrypt", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// KeyServiceServer is the server API for KeyService service.
+type KeyServiceServer interface {
+ Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error)
+ Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error)
+}
+
+// UnimplementedKeyServiceServer can be embedded to have forward compatible implementations.
+type UnimplementedKeyServiceServer struct {
+}
+
+func (*UnimplementedKeyServiceServer) Encrypt(context.Context, *EncryptRequest) (*EncryptResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Encrypt not implemented")
+}
+func (*UnimplementedKeyServiceServer) Decrypt(context.Context, *DecryptRequest) (*DecryptResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Decrypt not implemented")
+}
+
+func RegisterKeyServiceServer(s *grpc.Server, srv KeyServiceServer) {
+ s.RegisterService(&_KeyService_serviceDesc, srv)
+}
+
+func _KeyService_Encrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(EncryptRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(KeyServiceServer).Encrypt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/KeyService/Encrypt",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(KeyServiceServer).Encrypt(ctx, req.(*EncryptRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _KeyService_Decrypt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DecryptRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(KeyServiceServer).Decrypt(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/KeyService/Decrypt",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(KeyServiceServer).Decrypt(ctx, req.(*DecryptRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _KeyService_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "KeyService",
+ HandlerType: (*KeyServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "Encrypt",
+ Handler: _KeyService_Encrypt_Handler,
+ },
+ {
+ MethodName: "Decrypt",
+ Handler: _KeyService_Decrypt_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "keyservice/keyservice.proto",
+}
diff --git a/keyservice/keyservice/keyservice.proto b/keyservice/keyservice/keyservice.proto
new file mode 100644
index 000000000..1d91a5709
--- /dev/null
+++ b/keyservice/keyservice/keyservice.proto
@@ -0,0 +1,66 @@
+syntax = "proto3";
+
+message Key {
+ oneof key_type {
+ KmsKey kms_key = 1;
+ PgpKey pgp_key = 2;
+ GcpKmsKey gcp_kms_key = 3;
+ AzureKeyVaultKey azure_keyvault_key = 4;
+ VaultKey vault_key = 5;
+ AgeKey age_key = 6;
+ }
+}
+
+message PgpKey {
+ string fingerprint = 1;
+}
+
+message KmsKey {
+ string arn = 1;
+ string role = 2;
+ map context = 3;
+ string aws_profile = 4;
+}
+
+message GcpKmsKey {
+ string resource_id = 1;
+}
+
+message VaultKey {
+ string vault_address = 1;
+ string engine_path = 2;
+ string key_name = 3;
+}
+
+message AzureKeyVaultKey {
+ string vault_url = 1;
+ string name = 2;
+ string version = 3;
+}
+
+message AgeKey {
+ string recipient = 1;
+}
+
+message EncryptRequest {
+ Key key = 1;
+ bytes plaintext = 2;
+}
+
+message EncryptResponse {
+ bytes ciphertext = 1;
+}
+
+message DecryptRequest {
+ Key key = 1;
+ bytes ciphertext = 2;
+}
+
+message DecryptResponse {
+ bytes plaintext = 1;
+}
+
+service KeyService {
+ rpc Encrypt (EncryptRequest) returns (EncryptResponse) {}
+ rpc Decrypt (DecryptRequest) returns (DecryptResponse) {}
+}
diff --git a/keyservice/keyservice/server.go b/keyservice/keyservice/server.go
new file mode 100644
index 000000000..9f2b486a6
--- /dev/null
+++ b/keyservice/keyservice/server.go
@@ -0,0 +1,327 @@
+package keyservice
+
+import (
+ "fmt"
+
+ "github.com/getsops/sops/v3/age"
+ "github.com/getsops/sops/v3/azkv"
+ "github.com/getsops/sops/v3/gcpkms"
+ "github.com/getsops/sops/v3/hcvault"
+ "github.com/getsops/sops/v3/kms"
+ "github.com/getsops/sops/v3/pgp"
+ "golang.org/x/net/context"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+)
+
+// Server is a key service server that uses SOPS MasterKeys to fulfill requests
+type Server struct {
+ // Prompt indicates whether the server should prompt before decrypting or encrypting data
+ Prompt bool
+}
+
+func (ks *Server) encryptWithPgp(key *PgpKey, plaintext []byte) ([]byte, error) {
+ pgpKey := pgp.NewMasterKeyFromFingerprint(key.Fingerprint)
+ err := pgpKey.Encrypt(plaintext)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(pgpKey.EncryptedKey), nil
+}
+
+func (ks *Server) encryptWithKms(key *KmsKey, plaintext []byte) ([]byte, error) {
+ kmsKey := kmsKeyToMasterKey(key)
+ err := kmsKey.Encrypt(plaintext)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(kmsKey.EncryptedKey), nil
+}
+
+func (ks *Server) encryptWithGcpKms(key *GcpKmsKey, plaintext []byte) ([]byte, error) {
+ gcpKmsKey := gcpkms.MasterKey{
+ ResourceID: key.ResourceId,
+ }
+ err := gcpKmsKey.Encrypt(plaintext)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(gcpKmsKey.EncryptedKey), nil
+}
+
+func (ks *Server) encryptWithAzureKeyVault(key *AzureKeyVaultKey, plaintext []byte) ([]byte, error) {
+ azkvKey := azkv.MasterKey{
+ VaultURL: key.VaultUrl,
+ Name: key.Name,
+ Version: key.Version,
+ }
+ err := azkvKey.Encrypt(plaintext)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(azkvKey.EncryptedKey), nil
+}
+
+func (ks *Server) encryptWithVault(key *VaultKey, plaintext []byte) ([]byte, error) {
+ vaultKey := hcvault.MasterKey{
+ VaultAddress: key.VaultAddress,
+ EnginePath: key.EnginePath,
+ KeyName: key.KeyName,
+ }
+ err := vaultKey.Encrypt(plaintext)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(vaultKey.EncryptedKey), nil
+}
+
+func (ks *Server) encryptWithAge(key *AgeKey, plaintext []byte) ([]byte, error) {
+ ageKey := age.MasterKey{
+ Recipient: key.Recipient,
+ }
+
+ if err := ageKey.Encrypt(plaintext); err != nil {
+ return nil, err
+ }
+
+ return []byte(ageKey.EncryptedKey), nil
+}
+
+func (ks *Server) decryptWithPgp(key *PgpKey, ciphertext []byte) ([]byte, error) {
+ pgpKey := pgp.NewMasterKeyFromFingerprint(key.Fingerprint)
+ pgpKey.EncryptedKey = string(ciphertext)
+ plaintext, err := pgpKey.Decrypt()
+ return []byte(plaintext), err
+}
+
+func (ks *Server) decryptWithKms(key *KmsKey, ciphertext []byte) ([]byte, error) {
+ kmsKey := kmsKeyToMasterKey(key)
+ kmsKey.EncryptedKey = string(ciphertext)
+ plaintext, err := kmsKey.Decrypt()
+ return []byte(plaintext), err
+}
+
+func (ks *Server) decryptWithGcpKms(key *GcpKmsKey, ciphertext []byte) ([]byte, error) {
+ gcpKmsKey := gcpkms.MasterKey{
+ ResourceID: key.ResourceId,
+ }
+ gcpKmsKey.EncryptedKey = string(ciphertext)
+ plaintext, err := gcpKmsKey.Decrypt()
+ return []byte(plaintext), err
+}
+
+func (ks *Server) decryptWithAzureKeyVault(key *AzureKeyVaultKey, ciphertext []byte) ([]byte, error) {
+ azkvKey := azkv.MasterKey{
+ VaultURL: key.VaultUrl,
+ Name: key.Name,
+ Version: key.Version,
+ }
+ azkvKey.EncryptedKey = string(ciphertext)
+ plaintext, err := azkvKey.Decrypt()
+ return []byte(plaintext), err
+}
+
+func (ks *Server) decryptWithVault(key *VaultKey, ciphertext []byte) ([]byte, error) {
+ vaultKey := hcvault.MasterKey{
+ VaultAddress: key.VaultAddress,
+ EnginePath: key.EnginePath,
+ KeyName: key.KeyName,
+ }
+ vaultKey.EncryptedKey = string(ciphertext)
+ plaintext, err := vaultKey.Decrypt()
+ return []byte(plaintext), err
+}
+
+func (ks *Server) decryptWithAge(key *AgeKey, ciphertext []byte) ([]byte, error) {
+ ageKey := age.MasterKey{
+ Recipient: key.Recipient,
+ }
+ ageKey.EncryptedKey = string(ciphertext)
+ plaintext, err := ageKey.Decrypt()
+ return []byte(plaintext), err
+}
+
+// Encrypt takes an encrypt request and encrypts the provided plaintext with the provided key, returning the encrypted
+// result
+func (ks Server) Encrypt(ctx context.Context,
+ req *EncryptRequest) (*EncryptResponse, error) {
+ key := req.Key
+ var response *EncryptResponse
+ switch k := key.KeyType.(type) {
+ case *Key_PgpKey:
+ ciphertext, err := ks.encryptWithPgp(k.PgpKey, req.Plaintext)
+ if err != nil {
+ return nil, err
+ }
+ response = &EncryptResponse{
+ Ciphertext: ciphertext,
+ }
+ case *Key_KmsKey:
+ ciphertext, err := ks.encryptWithKms(k.KmsKey, req.Plaintext)
+ if err != nil {
+ return nil, err
+ }
+ response = &EncryptResponse{
+ Ciphertext: ciphertext,
+ }
+ case *Key_GcpKmsKey:
+ ciphertext, err := ks.encryptWithGcpKms(k.GcpKmsKey, req.Plaintext)
+ if err != nil {
+ return nil, err
+ }
+ response = &EncryptResponse{
+ Ciphertext: ciphertext,
+ }
+ case *Key_AzureKeyvaultKey:
+ ciphertext, err := ks.encryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Plaintext)
+ if err != nil {
+ return nil, err
+ }
+ response = &EncryptResponse{
+ Ciphertext: ciphertext,
+ }
+ case *Key_VaultKey:
+ ciphertext, err := ks.encryptWithVault(k.VaultKey, req.Plaintext)
+ if err != nil {
+ return nil, err
+ }
+ response = &EncryptResponse{
+ Ciphertext: ciphertext,
+ }
+ case *Key_AgeKey:
+ ciphertext, err := ks.encryptWithAge(k.AgeKey, req.Plaintext)
+ if err != nil {
+ return nil, err
+ }
+ response = &EncryptResponse{
+ Ciphertext: ciphertext,
+ }
+ case nil:
+ return nil, status.Errorf(codes.NotFound, "Must provide a key")
+ default:
+ return nil, status.Errorf(codes.NotFound, "Unknown key type")
+ }
+ if ks.Prompt {
+ err := ks.prompt(key, "encrypt")
+ if err != nil {
+ return nil, err
+ }
+ }
+ return response, nil
+}
+
+func keyToString(key *Key) string {
+ switch k := key.KeyType.(type) {
+ case *Key_PgpKey:
+ return fmt.Sprintf("PGP key with fingerprint %s", k.PgpKey.Fingerprint)
+ case *Key_KmsKey:
+ return fmt.Sprintf("AWS KMS key with ARN %s", k.KmsKey.Arn)
+ case *Key_GcpKmsKey:
+ return fmt.Sprintf("GCP KMS key with resource ID %s", k.GcpKmsKey.ResourceId)
+ case *Key_AzureKeyvaultKey:
+ return fmt.Sprintf("Azure Key Vault key with URL %s/keys/%s/%s", k.AzureKeyvaultKey.VaultUrl, k.AzureKeyvaultKey.Name, k.AzureKeyvaultKey.Version)
+ case *Key_VaultKey:
+ return fmt.Sprintf("Hashicorp Vault key with URI %s/v1/%s/keys/%s", k.VaultKey.VaultAddress, k.VaultKey.EnginePath, k.VaultKey.KeyName)
+ default:
+ return "Unknown key type"
+ }
+}
+
+func (ks Server) prompt(key *Key, requestType string) error {
+ keyString := keyToString(key)
+ var response string
+ for response != "y" && response != "n" {
+ fmt.Printf("\nReceived %s request using %s. Respond to request? (y/n): ", requestType, keyString)
+ _, err := fmt.Scanln(&response)
+ if err != nil {
+ return err
+ }
+ }
+ if response == "n" {
+ return status.Errorf(codes.PermissionDenied, "Request rejected by user")
+ }
+ return nil
+}
+
+// Decrypt takes a decrypt request and decrypts the provided ciphertext with the provided key, returning the decrypted
+// result
+func (ks Server) Decrypt(ctx context.Context,
+ req *DecryptRequest) (*DecryptResponse, error) {
+ key := req.Key
+ var response *DecryptResponse
+ switch k := key.KeyType.(type) {
+ case *Key_PgpKey:
+ plaintext, err := ks.decryptWithPgp(k.PgpKey, req.Ciphertext)
+ if err != nil {
+ return nil, err
+ }
+ response = &DecryptResponse{
+ Plaintext: plaintext,
+ }
+ case *Key_KmsKey:
+ plaintext, err := ks.decryptWithKms(k.KmsKey, req.Ciphertext)
+ if err != nil {
+ return nil, err
+ }
+ response = &DecryptResponse{
+ Plaintext: plaintext,
+ }
+ case *Key_GcpKmsKey:
+ plaintext, err := ks.decryptWithGcpKms(k.GcpKmsKey, req.Ciphertext)
+ if err != nil {
+ return nil, err
+ }
+ response = &DecryptResponse{
+ Plaintext: plaintext,
+ }
+ case *Key_AzureKeyvaultKey:
+ plaintext, err := ks.decryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Ciphertext)
+ if err != nil {
+ return nil, err
+ }
+ response = &DecryptResponse{
+ Plaintext: plaintext,
+ }
+ case *Key_VaultKey:
+ plaintext, err := ks.decryptWithVault(k.VaultKey, req.Ciphertext)
+ if err != nil {
+ return nil, err
+ }
+ response = &DecryptResponse{
+ Plaintext: plaintext,
+ }
+ case *Key_AgeKey:
+ plaintext, err := ks.decryptWithAge(k.AgeKey, req.Ciphertext)
+ if err != nil {
+ return nil, err
+ }
+ response = &DecryptResponse{
+ Plaintext: plaintext,
+ }
+ case nil:
+ return nil, status.Errorf(codes.NotFound, "Must provide a key")
+ default:
+ return nil, status.Errorf(codes.NotFound, "Unknown key type")
+ }
+ if ks.Prompt {
+ err := ks.prompt(key, "decrypt")
+ if err != nil {
+ return nil, err
+ }
+ }
+ return response, nil
+}
+
+func kmsKeyToMasterKey(key *KmsKey) kms.MasterKey {
+ ctx := make(map[string]*string)
+ for k, v := range key.Context {
+ value := v // Allocate a new string to prevent the pointer below from referring to only the last iteration value
+ ctx[k] = &value
+ }
+ return kms.MasterKey{
+ Arn: key.Arn,
+ Role: key.Role,
+ EncryptionContext: ctx,
+ AwsProfile: key.AwsProfile,
+ }
+}
diff --git a/keyservice/keyservice/server_test.go b/keyservice/keyservice/server_test.go
new file mode 100644
index 000000000..cc29c4528
--- /dev/null
+++ b/keyservice/keyservice/server_test.go
@@ -0,0 +1,81 @@
+package keyservice
+
+import (
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestKmsKeyToMasterKey(t *testing.T) {
+
+ cases := []struct {
+ description string
+ expectedArn string
+ expectedRole string
+ expectedCtx map[string]string
+ expectedAwsProfile string
+ }{
+ {
+ description: "empty context",
+ expectedArn: "arn:aws:kms:eu-west-1:123456789012:key/d5c90a06-f824-4628-922b-12424571ed4d",
+ expectedRole: "ExampleRole",
+ expectedCtx: map[string]string{},
+ expectedAwsProfile: "",
+ },
+ {
+ description: "context with one key-value pair",
+ expectedArn: "arn:aws:kms:eu-west-1:123456789012:key/d5c90a06-f824-4628-922b-12424571ed4d",
+ expectedRole: "",
+ expectedCtx: map[string]string{
+ "firstKey": "first value",
+ },
+ expectedAwsProfile: "ExampleProfile",
+ },
+ {
+ description: "context with three key-value pairs",
+ expectedArn: "arn:aws:kms:eu-west-1:123456789012:key/d5c90a06-f824-4628-922b-12424571ed4d",
+ expectedRole: "",
+ expectedCtx: map[string]string{
+ "firstKey": "first value",
+ "secondKey": "second value",
+ "thirdKey": "third value",
+ },
+ expectedAwsProfile: "",
+ },
+ }
+
+ for _, c := range cases {
+
+ t.Run(c.description, func(t *testing.T) {
+
+ inputCtx := make(map[string]string)
+ for k, v := range c.expectedCtx {
+ inputCtx[k] = v
+ }
+
+ key := &KmsKey{
+ Arn: c.expectedArn,
+ Role: c.expectedRole,
+ Context: inputCtx,
+ AwsProfile: c.expectedAwsProfile,
+ }
+
+ masterKey := kmsKeyToMasterKey(key)
+ foundCtx := masterKey.EncryptionContext
+
+ for k := range c.expectedCtx {
+ require.Containsf(t, foundCtx, k, "Context does not contain expected key '%s'", k)
+ }
+ for k := range foundCtx {
+ require.Containsf(t, c.expectedCtx, k, "Context contains an unexpected key '%s' which cannot be found from expected map", k)
+ }
+ for k, expected := range c.expectedCtx {
+ foundVal := *foundCtx[k]
+ assert.Equalf(t, expected, foundVal, "Context key '%s' value '%s' does not match expected value '%s'", k, foundVal, expected)
+ }
+ assert.Equalf(t, c.expectedArn, masterKey.Arn, "Expected ARN to be '%s', but found '%s'", c.expectedArn, masterKey.Arn)
+ assert.Equalf(t, c.expectedRole, masterKey.Role, "Expected Role to be '%s', but found '%s'", c.expectedRole, masterKey.Role)
+ assert.Equalf(t, c.expectedAwsProfile, masterKey.AwsProfile, "Expected AWS profile to be '%s', but found '%s'", c.expectedAwsProfile, masterKey.AwsProfile)
+ })
+ }
+}
diff --git a/keyservice/server.go b/keyservice/server.go
index a20959062..9f2b486a6 100644
--- a/keyservice/server.go
+++ b/keyservice/server.go
@@ -20,8 +20,6 @@ type Server struct {
Prompt bool
}
-func (ks *Server) mustEmbedUnimplementedKeyServiceServer() {}
-
func (ks *Server) encryptWithPgp(key *PgpKey, plaintext []byte) ([]byte, error) {
pgpKey := pgp.NewMasterKeyFromFingerprint(key.Fingerprint)
err := pgpKey.Encrypt(plaintext)
@@ -42,8 +40,7 @@ func (ks *Server) encryptWithKms(key *KmsKey, plaintext []byte) ([]byte, error)
func (ks *Server) encryptWithGcpKms(key *GcpKmsKey, plaintext []byte) ([]byte, error) {
gcpKmsKey := gcpkms.MasterKey{
- ResourceID: key.ResourceId,
- AccessToken: gcpkms.AccessToken(key.AccessToken),
+ ResourceID: key.ResourceId,
}
err := gcpKmsKey.Encrypt(plaintext)
if err != nil {
@@ -106,8 +103,7 @@ func (ks *Server) decryptWithKms(key *KmsKey, ciphertext []byte) ([]byte, error)
func (ks *Server) decryptWithGcpKms(key *GcpKmsKey, ciphertext []byte) ([]byte, error) {
gcpKmsKey := gcpkms.MasterKey{
- ResourceID: key.ResourceId,
- AccessToken: gcpkms.AccessToken(key.AccessToken),
+ ResourceID: key.ResourceId,
}
gcpKmsKey.EncryptedKey = string(ciphertext)
plaintext, err := gcpKmsKey.Decrypt()
diff --git a/shamir/shamir/LICENSE b/shamir/shamir/LICENSE
new file mode 100644
index 000000000..be2cc4dfb
--- /dev/null
+++ b/shamir/shamir/LICENSE
@@ -0,0 +1,362 @@
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
diff --git a/shamir/shamir/README.md b/shamir/shamir/README.md
new file mode 100644
index 000000000..256077de7
--- /dev/null
+++ b/shamir/shamir/README.md
@@ -0,0 +1,165 @@
+# Shamir's secret sharing
+
+Forked from [Vault](https://github.com/hashicorp/vault/tree/master/shamir)
+
+## How it works
+
+We want to split a secret into parts.
+
+Any two points on the cartesian plane define a line. Three points define a
+parabola. Four points define a cubic curve, and so on. In general, `n` points
+define an function of degree `(n - 1)`. If our secret was somehow an function of
+degree `(n - 1)`, we could just compute `n` different points of that function
+and give `n` different people one point each. In order to recover the secret,
+then we'd need all the `n` points. If we wanted to, we could compute more than n
+points, but even then still only `n` points out of our whole set of computed
+points would be required to recover the function.
+
+A concrete example: our secret is the function `y = 2x + 1`. This function is of
+degree 1, so we need at least 2 points to define it. For example, let's set
+`x = 1`, `x = 2` and `x = 3`. From this follows that `y = 3`, `y = 5` and
+`y = 7`, respectively. Now, with the information that our secret is of degree 1,
+we can use any 2 of the 3 points we computed to recover our original function.
+For example, let's use the points `x = 1; y = 3` and `x = 2; y = 5`.
+We know that first degree functions are lines, defined by their slope and their
+intersection point with the y axis. We can easily compute the slope given our
+two points: it's the change in `y` divided by the change in `x`:
+`(5 - 3)/(2 - 1) = 2`. Now, knowing the slope we can compute the intersection
+point with the `y` axis by "working our way back". We know that at `x = 1`,
+`y` equals `3`, so naturally because the slope is `2`, at `x = 0`, `y` must be
+`1`.
+
+## Lagrange interpolation
+
+The method we've used for this isn't very general: it only works for polynomials
+of degree 1. Lagrange interpolation is a more general way that lets us obtain
+the function of degree `(n - 1)` that passes through `n` arbitrary points.
+
+Understanding how to perform Lagrange interpolation isn't really necessary to
+understand Shamir's Secret Sharing: it's enough to know that there's only one
+function of degree `(n - 1)` that passes through `n` given points and that
+computing this function given the points is computationally efficient.
+
+But for those interested, here's an explanation:
+
+Let's say our points are `(x_0, y_0),...,(x_j, y_j),...,(x_(n-1), y_(n-1))`.
+Then, the Lagrange polynomial `L(x)`, the polynomial we're looking for, is
+defined as follows:
+
+`L(x) = sum from j=0 to j=(n-1) of {y_j * l_j(x)}`
+
+and `l_j(x) = product from m=0 to m=(n-1) except when m=j of {(x - x_m)/(x_j - x_m)}`
+
+A concrete example, with 3 points:
+
+```
+x_0 = 1 y_0 = 1
+x_1 = 2 y_1 = 4
+x_2 = 3 y_2 = 9
+```
+
+Let's apply the formula:
+
+```
+L(x) =
+ y_0 * l_0(x) +
+ y_1 * l_1(x) +
+ y_2 * l_2(x)
+```
+
+Substitute `y_j` for the actual value:
+
+```
+L(x) =
+ 1 * l_0(x) +
+ 4 * l_1(x) +
+ 9 * l_2(x)
+```
+
+Replace `l_j(x)`:
+
+```
+l_0(x) = (x - 2)/(1 - 2) * (x - 3)/(1 - 3) = 0.5x^2 - 2.5x + 3
+l_1(x) = (x - 1)/(2 - 1) * (x - 3)/(2 - 3) = - x^2 + 4x - 3
+l_2(x) = (x - 1)/(3 - 1) * (x - 2)/(3 - 2) = 0.5x^2 - 1.5x + 1
+```
+
+```
+
+L(x) =
+ 1 * ( 0.5x^2 - 2.5x + 3) +
+ 4 * ( -x^2 + 4x - 3) +
+ 9 * ( 0.5x^2 - 1.5x + 1)
+```
+
+```
+
+L(x) =
+ ( 0.5x^2 - 2.5x + 3) +
+ ( -4x^2 + 16x - 12) +
+ ( 4.5x^2 - 13.5x + 9)
+ = x^2 + 0x + 0
+ = x^2
+```
+
+So the polynomial we were looking for is `y = x^2`.
+
+## Splitting a secret
+
+So we have the ability of splitting a function into parts, but in the context
+of computing we generally want to split a number, not a function. For this,
+let's define a function of degree `threshold`. `threshold` is the amount of
+parts we want to require in order to recover the secret. Let's set the parameter
+of degree zero to our secret `S` and make the rest of the parameters random:
+
+`y = ax^(threshold) + bx^(threshold-1) + ... + zx^1 + S`
+
+With `a, b, ...` random.
+
+Then, we want to generate our parts. For this, we evaluate our function at as
+many points as we want parts. For example, say our secret is 123, we want 5
+parts and a threshold of 2. Because the threshold is 2, we're going to need a
+polynomial of degree 2:
+
+`y = ax^2 + bx + 123`
+
+We randomly set `a = 7` and `b = 1`:
+
+`y = 7x^2 + x + 123`
+
+Because we want 5 parts, we need to compute 5 points:
+
+```
+x = 0 -> y = 123 # woops! This is the secret itself. Let's not use that one.
+x = 1 -> y = 131
+x = 2 -> y = 153
+x = 3 -> y = 189
+x = 4 -> y = 239
+x = 5 -> y = 303
+```
+
+And that's it. Each of the computed points is one part of the secret.
+
+## Combining a secret
+
+Now that we have our parts, we have to define a way to recover them. Using
+the example from the previous section, we only need any two points out of the
+five we created to recover the secret, because we set the threshold to two.
+So with any two of the five points we created, we can recover the original
+polynomial, and because the secret is the free term in the polynomial, we can
+recover the secret.
+
+## Finite fields
+
+In the previous examples we've only used integers, and this unfortunately has
+a flaw. First of all, it's impossible to uniformly sample integers to get
+random coefficients for our generated polynomial. Additionally, if we don't
+operate in a finite field, information about the secret is leaked for every part
+someone recovers.
+
+For these reasons, Vault's implementation of Shamir's Secret Sharing uses finite
+field arithmetic, specifically in GF(2^8), with 229 as the generator. GF(2^8)
+has 256 elements, so using this we can only split one byte at a time. This is
+not a problem, though, as we can just split each byte in our secret
+independently. This implementation uses tables to speed up the execution of
+finite field arithmetic.
diff --git a/shamir/shamir/shamir.go b/shamir/shamir/shamir.go
new file mode 100644
index 000000000..10d7bc3ba
--- /dev/null
+++ b/shamir/shamir/shamir.go
@@ -0,0 +1,293 @@
+package shamir
+
+// Some comments in this file were written by @autrilla
+// The code was written by HashiCorp as part of Vault.
+
+// This implementation of Shamir's Secret Sharing matches the definition
+// of the scheme. Other tools used, such as GF(2^8) arithmetic, Lagrange
+// interpolation and Horner's method also match their definitions and should
+// therefore be correct.
+// More information about Shamir's Secret Sharing and Lagrange interpolation
+// can be found in README.md
+
+import (
+ "crypto/rand"
+ "crypto/subtle"
+ "fmt"
+ mathrand "math/rand"
+ "time"
+)
+
+const (
+ // ShareOverhead is the byte size overhead of each share
+ // when using Split on a secret. This is caused by appending
+ // a one byte tag to the share.
+ ShareOverhead = 1
+)
+
+// polynomial represents a polynomial of arbitrary degree
+type polynomial struct {
+ coefficients []uint8
+}
+
+// makePolynomial constructs a random polynomial of the given
+// degree but with the provided intercept value.
+func makePolynomial(intercept, degree uint8) (polynomial, error) {
+ // Create a wrapper
+ p := polynomial{
+ coefficients: make([]byte, degree+1),
+ }
+
+ // Ensure the intercept is set
+ p.coefficients[0] = intercept
+
+ // Assign random co-efficients to the polynomial
+ if _, err := rand.Read(p.coefficients[1:]); err != nil {
+ return p, err
+ }
+
+ return p, nil
+}
+
+// evaluate returns the value of the polynomial for the given x
+// Uses Horner's method to
+// evaluate the polynomial at point x
+func (p *polynomial) evaluate(x uint8) uint8 {
+ // Special case the origin
+ if x == 0 {
+ return p.coefficients[0]
+ }
+
+ // Compute the polynomial value using Horner's method.
+ degree := len(p.coefficients) - 1
+ out := p.coefficients[degree]
+ for i := degree - 1; i >= 0; i-- {
+ coeff := p.coefficients[i]
+ out = add(mult(out, x), coeff)
+ }
+ return out
+}
+
+// interpolatePolynomial takes N sample points and returns
+// the value at a given x using a lagrange interpolation.
+// An implementation of Lagrange interpolation
+//
+// For this particular implementation, x is always 0
+func interpolatePolynomial(xSamples, ySamples []uint8, x uint8) uint8 {
+ limit := len(xSamples)
+ var result, basis uint8
+ for i := 0; i < limit; i++ {
+ basis = 1
+ for j := 0; j < limit; j++ {
+ if i == j {
+ continue
+ }
+ num := add(x, xSamples[j])
+ denom := add(xSamples[i], xSamples[j])
+ term := div(num, denom)
+ basis = mult(basis, term)
+ }
+ group := mult(ySamples[i], basis)
+ result = add(result, group)
+ }
+ return result
+}
+
+// div divides two numbers in GF(2^8)
+// GF(2^8) division using log/exp tables
+func div(a, b uint8) uint8 {
+ if b == 0 {
+ // leaks some timing information but we don't care anyways as this
+ // should never happen, hence the panic
+ panic("divide by zero")
+ }
+
+ var goodVal, zero uint8
+ logA := logTable[a]
+ logB := logTable[b]
+ diff := (int(logA) - int(logB)) % 255
+ if diff < 0 {
+ diff += 255
+ }
+
+ ret := expTable[diff]
+
+ // Ensure we return zero if a is zero but aren't subject to timing attacks
+ goodVal = ret
+
+ if subtle.ConstantTimeByteEq(a, 0) == 1 {
+ ret = zero
+ } else {
+ ret = goodVal
+ }
+
+ return ret
+}
+
+// mult multiplies two numbers in GF(2^8)
+// GF(2^8) multiplication using log/exp tables
+func mult(a, b uint8) (out uint8) {
+ var goodVal, zero uint8
+ log_a := logTable[a]
+ log_b := logTable[b]
+ sum := (int(log_a) + int(log_b)) % 255
+
+ ret := expTable[sum]
+
+ // Ensure we return zero if either a or b are zero but aren't subject to
+ // timing attacks
+ goodVal = ret
+
+ if subtle.ConstantTimeByteEq(a, 0) == 1 {
+ ret = zero
+ } else {
+ ret = goodVal
+ }
+
+ if subtle.ConstantTimeByteEq(b, 0) == 1 {
+ ret = zero
+ } else {
+ // This operation does not do anything logically useful. It
+ // only ensures a constant number of assignments to thwart
+ // timing attacks.
+ goodVal = zero
+ }
+
+ return ret
+}
+
+// add combines two numbers in GF(2^8)
+// This can also be used for subtraction since it is symmetric.
+func add(a, b uint8) uint8 {
+ return a ^ b
+}
+
+// Split takes an arbitrarily long secret and generates a `parts`
+// number of shares, `threshold` of which are required to reconstruct
+// the secret. The parts and threshold must be at least 2, and less
+// than 256. The returned shares are each one byte longer than the secret
+// as they attach a tag used to reconstruct the secret.
+func Split(secret []byte, parts, threshold int) ([][]byte, error) {
+ // Sanity check the input
+ if parts < threshold {
+ return nil, fmt.Errorf("parts cannot be less than threshold")
+ }
+ if parts > 255 {
+ return nil, fmt.Errorf("parts cannot exceed 255")
+ }
+ if threshold < 2 {
+ return nil, fmt.Errorf("threshold must be at least 2")
+ }
+ if threshold > 255 {
+ return nil, fmt.Errorf("threshold cannot exceed 255")
+ }
+ if len(secret) == 0 {
+ return nil, fmt.Errorf("cannot split an empty secret")
+ }
+
+ // Generate random x coordinates for computing points. I don't know
+ // why random x coordinates are used, and I also don't know why
+ // a non-cryptographically secure source of randomness is used.
+ // As far as I know the x coordinates do not need to be random.
+
+ mathrand.Seed(time.Now().UnixNano())
+ xCoordinates := mathrand.Perm(255)
+
+ // Allocate the output array, initialize the final byte
+ // of the output with the offset. The representation of each
+ // output is {y1, y2, .., yN, x}.
+ out := make([][]byte, parts)
+ for idx := range out {
+ // Store the x coordinate for each part as its last byte
+ // Add 1 to the xCoordinate because if the x coordinate is 0,
+ // then the result of evaluating the polynomial at that point
+ // will be our secret
+ out[idx] = make([]byte, len(secret)+1)
+ out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1
+ }
+
+ // Construct a random polynomial for each byte of the secret.
+ // Because we are using a field of size 256, we can only represent
+ // a single byte as the intercept of the polynomial, so we must
+ // use a new polynomial for each byte.
+ for idx, val := range secret {
+ // Create a random polynomial for each point.
+ // This polynomial crosses the y axis at `val`.
+ p, err := makePolynomial(val, uint8(threshold-1))
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate polynomial: %w", err)
+ }
+
+ // Generate a `parts` number of (x,y) pairs
+ // We cheat by encoding the x value once as the final index,
+ // so that it only needs to be stored once.
+ for i := 0; i < parts; i++ {
+ // Add 1 to the xCoordinate because if it's 0,
+ // then the result of p.evaluate(x) will be our secret
+ x := uint8(xCoordinates[i]) + 1
+ // Evaluate the polynomial at x
+ y := p.evaluate(x)
+ out[i][idx] = y
+ }
+ }
+
+ // Return the encoded secrets
+ return out, nil
+}
+
+// Combine is used to reverse a Split and reconstruct a secret
+// once a `threshold` number of parts are available.
+func Combine(parts [][]byte) ([]byte, error) {
+ // Verify enough parts provided
+ if len(parts) < 2 {
+ return nil, fmt.Errorf("less than two parts cannot be used to reconstruct the secret")
+ }
+
+ // Verify the parts are all the same length
+ firstPartLen := len(parts[0])
+ if firstPartLen < 2 {
+ return nil, fmt.Errorf("parts must be at least two bytes")
+ }
+ for i := 1; i < len(parts); i++ {
+ if len(parts[i]) != firstPartLen {
+ return nil, fmt.Errorf("all parts must be the same length")
+ }
+ }
+
+ // Create a buffer to store the reconstructed secret
+ secret := make([]byte, firstPartLen-1)
+
+ // Buffer to store the samples
+ xSamples := make([]uint8, len(parts))
+ ySamples := make([]uint8, len(parts))
+
+ // Set the x value for each sample and ensure no x_sample values are the same,
+ // otherwise div() can be unhappy
+ // Check that we don't have any duplicate parts, that is, two or
+ // more parts with the same x coordinate.
+ checkMap := map[byte]bool{}
+ for i, part := range parts {
+ samp := part[firstPartLen-1]
+ if exists := checkMap[samp]; exists {
+ return nil, fmt.Errorf("duplicate part detected")
+ }
+ checkMap[samp] = true
+ xSamples[i] = samp
+ }
+
+ // Reconstruct each byte
+ for idx := range secret {
+ // Set the y value for each sample
+ for i, part := range parts {
+ ySamples[i] = part[idx]
+ }
+
+ // Use Lagrange interpolation to retrieve the free term
+ // of the original polynomial
+ val := interpolatePolynomial(xSamples, ySamples, 0)
+
+ // Evaluate the 0th value to get the intercept
+ secret[idx] = val
+ }
+ return secret, nil
+}
diff --git a/shamir/shamir/shamir_test.go b/shamir/shamir/shamir_test.go
new file mode 100644
index 000000000..18727a89d
--- /dev/null
+++ b/shamir/shamir/shamir_test.go
@@ -0,0 +1,198 @@
+package shamir
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestSplit_invalid(t *testing.T) {
+ secret := []byte("test")
+
+ if _, err := Split(secret, 0, 0); err == nil {
+ t.Fatalf("expect error")
+ }
+
+ if _, err := Split(secret, 2, 3); err == nil {
+ t.Fatalf("expect error")
+ }
+
+ if _, err := Split(secret, 1000, 3); err == nil {
+ t.Fatalf("expect error")
+ }
+
+ if _, err := Split(secret, 10, 1); err == nil {
+ t.Fatalf("expect error")
+ }
+
+ if _, err := Split(nil, 3, 2); err == nil {
+ t.Fatalf("expect error")
+ }
+}
+
+func TestSplit(t *testing.T) {
+ secret := []byte("test")
+
+ out, err := Split(secret, 5, 3)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ if len(out) != 5 {
+ t.Fatalf("bad: %v", out)
+ }
+
+ for _, share := range out {
+ if len(share) != len(secret)+1 {
+ t.Fatalf("bad: %v", out)
+ }
+ }
+}
+
+func TestCombine_invalid(t *testing.T) {
+ // Not enough parts
+ if _, err := Combine(nil); err == nil {
+ t.Fatalf("should err")
+ }
+
+ // Mismatch in length
+ parts := [][]byte{
+ []byte("foo"),
+ []byte("ba"),
+ }
+ if _, err := Combine(parts); err == nil {
+ t.Fatalf("should err")
+ }
+
+ //Too short
+ parts = [][]byte{
+ []byte("f"),
+ []byte("b"),
+ }
+ if _, err := Combine(parts); err == nil {
+ t.Fatalf("should err")
+ }
+
+ parts = [][]byte{
+ []byte("foo"),
+ []byte("foo"),
+ }
+ if _, err := Combine(parts); err == nil {
+ t.Fatalf("should err")
+ }
+}
+
+func TestCombine(t *testing.T) {
+ secret := []byte("test")
+
+ out, err := Split(secret, 5, 3)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ // There is 5*4*3 possible choices,
+ // we will just brute force try them all
+ for i := 0; i < 5; i++ {
+ for j := 0; j < 5; j++ {
+ if j == i {
+ continue
+ }
+ for k := 0; k < 5; k++ {
+ if k == i || k == j {
+ continue
+ }
+ parts := [][]byte{out[i], out[j], out[k]}
+ recomb, err := Combine(parts)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ if !bytes.Equal(recomb, secret) {
+ t.Errorf("parts: (i:%d, j:%d, k:%d) %v", i, j, k, parts)
+ t.Fatalf("bad: %v %v", recomb, secret)
+ }
+ }
+ }
+ }
+}
+
+func TestField_Add(t *testing.T) {
+ if out := add(16, 16); out != 0 {
+ t.Fatalf("Bad: %v 16", out)
+ }
+
+ if out := add(3, 4); out != 7 {
+ t.Fatalf("Bad: %v 7", out)
+ }
+}
+
+func TestField_Mult(t *testing.T) {
+ if out := mult(3, 7); out != 9 {
+ t.Fatalf("Bad: %v 9", out)
+ }
+
+ if out := mult(3, 0); out != 0 {
+ t.Fatalf("Bad: %v 0", out)
+ }
+
+ if out := mult(0, 3); out != 0 {
+ t.Fatalf("Bad: %v 0", out)
+ }
+}
+
+func TestField_Divide(t *testing.T) {
+ if out := div(0, 7); out != 0 {
+ t.Fatalf("Bad: %v 0", out)
+ }
+
+ if out := div(3, 3); out != 1 {
+ t.Fatalf("Bad: %v 1", out)
+ }
+
+ if out := div(6, 3); out != 2 {
+ t.Fatalf("Bad: %v 2", out)
+ }
+}
+
+func TestPolynomial_Random(t *testing.T) {
+ p, err := makePolynomial(42, 2)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ if p.coefficients[0] != 42 {
+ t.Fatalf("bad: %v", p.coefficients)
+ }
+}
+
+func TestPolynomial_Eval(t *testing.T) {
+ p, err := makePolynomial(42, 1)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ if out := p.evaluate(0); out != 42 {
+ t.Fatalf("bad: %v", out)
+ }
+
+ out := p.evaluate(1)
+ exp := add(42, mult(1, p.coefficients[1]))
+ if out != exp {
+ t.Fatalf("bad: %v %v %v", out, exp, p.coefficients)
+ }
+}
+
+func TestInterpolate_Rand(t *testing.T) {
+ for i := 0; i < 256; i++ {
+ p, err := makePolynomial(uint8(i), 2)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ xVals := []uint8{1, 2, 3}
+ yVals := []uint8{p.evaluate(1), p.evaluate(2), p.evaluate(3)}
+ out := interpolatePolynomial(xVals, yVals, 0)
+ if out != uint8(i) {
+ t.Fatalf("Bad: %v %d", out, i)
+ }
+ }
+}
diff --git a/shamir/shamir/tables.go b/shamir/shamir/tables.go
new file mode 100644
index 000000000..76c245e79
--- /dev/null
+++ b/shamir/shamir/tables.go
@@ -0,0 +1,77 @@
+package shamir
+
+// Tables taken from http://www.samiam.org/galois.html
+// They use 0xe5 (229) as the generator
+
+var (
+ // logTable provides the log(X)/log(g) at each index X
+ logTable = [256]uint8{
+ 0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36,
+ 0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18,
+ 0x23, 0x20, 0x07, 0x70, 0xa1, 0x6c, 0x0c, 0x7f,
+ 0x62, 0x8b, 0x40, 0x46, 0xc7, 0x4b, 0xe0, 0x0e,
+ 0xeb, 0x16, 0xe8, 0xad, 0xcf, 0xcd, 0x39, 0x53,
+ 0x6a, 0x27, 0x35, 0x93, 0xd4, 0x4e, 0x48, 0xc3,
+ 0x2b, 0x79, 0x54, 0x28, 0x09, 0x78, 0x0f, 0x21,
+ 0x90, 0x87, 0x14, 0x2a, 0xa9, 0x9c, 0xd6, 0x74,
+ 0xb4, 0x7c, 0xde, 0xed, 0xb1, 0x86, 0x76, 0xa4,
+ 0x98, 0xe2, 0x96, 0x8f, 0x02, 0x32, 0x1c, 0xc1,
+ 0x33, 0xee, 0xef, 0x81, 0xfd, 0x30, 0x5c, 0x13,
+ 0x9d, 0x29, 0x17, 0xc4, 0x11, 0x44, 0x8c, 0x80,
+ 0xf3, 0x73, 0x42, 0x1e, 0x1d, 0xb5, 0xf0, 0x12,
+ 0xd1, 0x5b, 0x41, 0xa2, 0xd7, 0x2c, 0xe9, 0xd5,
+ 0x59, 0xcb, 0x50, 0xa8, 0xdc, 0xfc, 0xf2, 0x56,
+ 0x72, 0xa6, 0x65, 0x2f, 0x9f, 0x9b, 0x3d, 0xba,
+ 0x7d, 0xc2, 0x45, 0x82, 0xa7, 0x57, 0xb6, 0xa3,
+ 0x7a, 0x75, 0x4f, 0xae, 0x3f, 0x37, 0x6d, 0x47,
+ 0x61, 0xbe, 0xab, 0xd3, 0x5f, 0xb0, 0x58, 0xaf,
+ 0xca, 0x5e, 0xfa, 0x85, 0xe4, 0x4d, 0x8a, 0x05,
+ 0xfb, 0x60, 0xb7, 0x7b, 0xb8, 0x26, 0x4a, 0x67,
+ 0xc6, 0x1a, 0xf8, 0x69, 0x25, 0xb3, 0xdb, 0xbd,
+ 0x66, 0xdd, 0xf1, 0xd2, 0xdf, 0x03, 0x8d, 0x34,
+ 0xd9, 0x92, 0x0d, 0x63, 0x55, 0xaa, 0x49, 0xec,
+ 0xbc, 0x95, 0x3c, 0x84, 0x0b, 0xf5, 0xe6, 0xe7,
+ 0xe5, 0xac, 0x7e, 0x6e, 0xb9, 0xf9, 0xda, 0x8e,
+ 0x9a, 0xc9, 0x24, 0xe1, 0x0a, 0x15, 0x6b, 0x3a,
+ 0xa0, 0x51, 0xf4, 0xea, 0xb2, 0x97, 0x9e, 0x5d,
+ 0x22, 0x88, 0x94, 0xce, 0x19, 0x01, 0x71, 0x4c,
+ 0xa5, 0xe3, 0xc5, 0x31, 0xbb, 0xcc, 0x1f, 0x2d,
+ 0x3b, 0x52, 0x6f, 0xf6, 0x2e, 0x89, 0xf7, 0xc0,
+ 0x68, 0x1b, 0x64, 0x04, 0x06, 0xbf, 0x83, 0x38}
+
+ // expTable provides the anti-log or exponentiation value
+ // for the equivalent index
+ expTable = [256]uint8{
+ 0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12,
+ 0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36,
+ 0x05, 0x5c, 0x67, 0x57, 0x3a, 0xd5, 0x21, 0x5a,
+ 0x0f, 0xe4, 0xa9, 0xf9, 0x4e, 0x64, 0x63, 0xee,
+ 0x11, 0x37, 0xe0, 0x10, 0xd2, 0xac, 0xa5, 0x29,
+ 0x33, 0x59, 0x3b, 0x30, 0x6d, 0xef, 0xf4, 0x7b,
+ 0x55, 0xeb, 0x4d, 0x50, 0xb7, 0x2a, 0x07, 0x8d,
+ 0xff, 0x26, 0xd7, 0xf0, 0xc2, 0x7e, 0x09, 0x8c,
+ 0x1a, 0x6a, 0x62, 0x0b, 0x5d, 0x82, 0x1b, 0x8f,
+ 0x2e, 0xbe, 0xa6, 0x1d, 0xe7, 0x9d, 0x2d, 0x8a,
+ 0x72, 0xd9, 0xf1, 0x27, 0x32, 0xbc, 0x77, 0x85,
+ 0x96, 0x70, 0x08, 0x69, 0x56, 0xdf, 0x99, 0x94,
+ 0xa1, 0x90, 0x18, 0xbb, 0xfa, 0x7a, 0xb0, 0xa7,
+ 0xf8, 0xab, 0x28, 0xd6, 0x15, 0x8e, 0xcb, 0xf2,
+ 0x13, 0xe6, 0x78, 0x61, 0x3f, 0x89, 0x46, 0x0d,
+ 0x35, 0x31, 0x88, 0xa3, 0x41, 0x80, 0xca, 0x17,
+ 0x5f, 0x53, 0x83, 0xfe, 0xc3, 0x9b, 0x45, 0x39,
+ 0xe1, 0xf5, 0x9e, 0x19, 0x5e, 0xb6, 0xcf, 0x4b,
+ 0x38, 0x04, 0xb9, 0x2b, 0xe2, 0xc1, 0x4a, 0xdd,
+ 0x48, 0x0c, 0xd0, 0x7d, 0x3d, 0x58, 0xde, 0x7c,
+ 0xd8, 0x14, 0x6b, 0x87, 0x47, 0xe8, 0x79, 0x84,
+ 0x73, 0x3c, 0xbd, 0x92, 0xc9, 0x23, 0x8b, 0x97,
+ 0x95, 0x44, 0xdc, 0xad, 0x40, 0x65, 0x86, 0xa2,
+ 0xa4, 0xcc, 0x7f, 0xec, 0xc0, 0xaf, 0x91, 0xfd,
+ 0xf7, 0x4f, 0x81, 0x2f, 0x5b, 0xea, 0xa8, 0x1c,
+ 0x02, 0xd1, 0x98, 0x71, 0xed, 0x25, 0xe3, 0x24,
+ 0x06, 0x68, 0xb3, 0x93, 0x2c, 0x6f, 0x3e, 0x6c,
+ 0x0a, 0xb8, 0xce, 0xae, 0x74, 0xb1, 0x42, 0xb4,
+ 0x1e, 0xd3, 0x49, 0xe9, 0x9c, 0xc8, 0xc6, 0xc7,
+ 0x22, 0x6e, 0xdb, 0x20, 0xbf, 0x43, 0x51, 0x52,
+ 0x66, 0xb2, 0x76, 0x60, 0xda, 0xc5, 0xf3, 0xf6,
+ 0xaa, 0xcd, 0x9a, 0xa0, 0x75, 0x54, 0x0e, 0x01}
+)
diff --git a/shamir/shamir/tables_test.go b/shamir/shamir/tables_test.go
new file mode 100644
index 000000000..81aa983b1
--- /dev/null
+++ b/shamir/shamir/tables_test.go
@@ -0,0 +1,13 @@
+package shamir
+
+import "testing"
+
+func TestTables(t *testing.T) {
+ for i := 1; i < 256; i++ {
+ logV := logTable[i]
+ expV := expTable[logV]
+ if expV != uint8(i) {
+ t.Fatalf("bad: %d log: %d exp: %d", i, logV, expV)
+ }
+ }
+}
diff --git a/stores/dotenv/store.go b/stores/dotenv/store.go
index 09f118aa8..1e533341e 100644
--- a/stores/dotenv/store.go
+++ b/stores/dotenv/store.go
@@ -134,7 +134,7 @@ func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) {
func (store *Store) EmitPlainFile(in sops.TreeBranches) ([]byte, error) {
buffer := bytes.Buffer{}
for _, item := range in[0] {
- if isComplexValue(item.Value) {
+ if IsComplexValue(item.Value) {
return nil, fmt.Errorf("cannot use complex value in dotenv file: %s", item.Value)
}
var line string
@@ -166,7 +166,7 @@ func (store *Store) EmitExample() []byte {
return bytes
}
-func isComplexValue(v interface{}) bool {
+func IsComplexValue(v interface{}) bool {
switch v.(type) {
case []interface{}:
return true
diff --git a/stores/stores.go b/stores/stores.go
index 9ab308d14..169e8dbb5 100644
--- a/stores/stores.go
+++ b/stores/stores.go
@@ -56,6 +56,8 @@ type Metadata struct {
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"`
+ UnencryptedCommentRegex string `yaml:"unencrypted_comment_regex,omitempty" json:"unencrypted_comment_regex,omitempty"`
+ EncryptedCommentRegex string `yaml:"encrypted_comment_regex,omitempty" json:"encrypted_comment_regex,omitempty"`
MACOnlyEncrypted bool `yaml:"mac_only_encrypted,omitempty" json:"mac_only_encrypted,omitempty"`
Version string `yaml:"version" json:"version"`
}
@@ -119,6 +121,8 @@ func MetadataFromInternal(sopsMetadata sops.Metadata) Metadata {
m.EncryptedSuffix = sopsMetadata.EncryptedSuffix
m.UnencryptedRegex = sopsMetadata.UnencryptedRegex
m.EncryptedRegex = sopsMetadata.EncryptedRegex
+ m.UnencryptedCommentRegex = sopsMetadata.UnencryptedCommentRegex
+ m.EncryptedCommentRegex = sopsMetadata.EncryptedCommentRegex
m.MessageAuthenticationCode = sopsMetadata.MessageAuthenticationCode
m.MACOnlyEncrypted = sopsMetadata.MACOnlyEncrypted
m.Version = sopsMetadata.Version
@@ -260,9 +264,15 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
if m.EncryptedRegex != "" {
cryptRuleCount++
}
+ if m.UnencryptedCommentRegex != "" {
+ cryptRuleCount++
+ }
+ if m.EncryptedCommentRegex != "" {
+ cryptRuleCount++
+ }
if cryptRuleCount > 1 {
- return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex or unencrypted_regex in the same file")
+ return sops.Metadata{}, fmt.Errorf("Cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file")
}
if cryptRuleCount == 0 {
@@ -277,6 +287,8 @@ func (m *Metadata) ToInternal() (sops.Metadata, error) {
EncryptedSuffix: m.EncryptedSuffix,
UnencryptedRegex: m.UnencryptedRegex,
EncryptedRegex: m.EncryptedRegex,
+ UnencryptedCommentRegex: m.UnencryptedCommentRegex,
+ EncryptedCommentRegex: m.EncryptedCommentRegex,
MACOnlyEncrypted: m.MACOnlyEncrypted,
LastModified: lastModified,
}, nil
diff --git a/version/version.go b/version/version.go
index 744b46f71..a40cc8ec8 100644
--- a/version/version.go
+++ b/version/version.go
@@ -12,7 +12,7 @@ import (
)
// Version represents the value of the current semantic version.
-var Version = "3.8.1"
+var Version = "3.9.0"
// PrintVersion prints the current version of sops. If the flag
// `--disable-version-check` is set, the function will not attempt
@@ -101,7 +101,7 @@ func RetrieveLatestVersionFromUpstream() (string, error) {
//
// Unlike RetrieveLatestVersionFromUpstream, it returns the tag (e.g. "v3.7.3").
func RetrieveLatestReleaseVersion() (tag, url string, err error) {
- const repository = "mozilla/sops"
+ const repository = "getsops/sops"
return newReleaseFetcher().LatestRelease(repository)
}