From e20a53d1a48ec8d53a266481c83602025ada078b Mon Sep 17 00:00:00 2001 From: Maren Sofie Ringsby Date: Wed, 7 Aug 2024 14:47:58 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20oppdater=20sops=20til=20=C3=A5=20kun?= =?UTF-8?q?=20endre=20p=C3=A5=20hvordan=20man=20kan=20bruke=20gcp=20tokens?= =?UTF-8?q?=20for=20=C3=A5=20gj=C3=B8re=20forskjellen=20s=C3=A5=20liten=20?= =?UTF-8?q?som=20mulig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/cmd/sops/codes/codes.go | 33 + cmd/cmd/sops/common/common.go | 449 ++++ cmd/cmd/sops/decrypt.go | 96 + cmd/cmd/sops/edit.go | 279 ++ cmd/cmd/sops/encrypt.go | 124 + cmd/cmd/sops/formats/formats.go | 78 + cmd/cmd/sops/formats/formats_test.go | 39 + cmd/cmd/sops/main.go | 2276 +++++++++++++++++ cmd/cmd/sops/rotate.go | 89 + cmd/cmd/sops/set.go | 65 + cmd/cmd/sops/subcommand/exec/exec.go | 149 ++ cmd/cmd/sops/subcommand/exec/exec_unix.go | 67 + cmd/cmd/sops/subcommand/exec/exec_windows.go | 22 + .../sops/subcommand/filestatus/filestatus.go | 60 + .../filestatus/filestatus_internal_test.go | 52 + cmd/cmd/sops/subcommand/groups/add.go | 54 + cmd/cmd/sops/subcommand/groups/delete.go | 63 + .../sops/subcommand/keyservice/keyservice.go | 52 + cmd/cmd/sops/subcommand/publish/publish.go | 189 ++ .../sops/subcommand/updatekeys/updatekeys.go | 125 + cmd/cmd/sops/unset.go | 67 + config/config.go | 186 +- config/config_test.go | 155 ++ decrypt/decrypt/decrypt.go | 80 + decrypt/decrypt/example_test.go | 55 + docs/docs/images/cncf-color-bg.svg | 1 + docs/docs/release.md | 75 + functional-tests/res/plainfile.yaml | 1 + functional-tests/src/lib.rs | 281 +- gcpkms/gcpkms/keysource.go | 280 ++ gcpkms/gcpkms/keysource_test.go | 168 ++ gcpkms/gcpkms/mock_kms_server_test.go | 326 +++ gcpkms/keysource.go | 60 +- hcvault/keysource.go | 2 +- keyservice/client.go | 2 +- keyservice/keyservice.go | 5 +- keyservice/keyservice.pb.go | 222 +- keyservice/keyservice.proto | 3 - keyservice/keyservice/client.go | 37 + keyservice/keyservice/keyservice.go | 84 + keyservice/keyservice/keyservice.pb.go | 1119 ++++++++ keyservice/keyservice/keyservice.proto | 66 + keyservice/keyservice/server.go | 327 +++ keyservice/keyservice/server_test.go | 81 + keyservice/server.go | 8 +- shamir/shamir/LICENSE | 362 +++ shamir/shamir/README.md | 165 ++ shamir/shamir/shamir.go | 293 +++ shamir/shamir/shamir_test.go | 198 ++ shamir/shamir/tables.go | 77 + shamir/shamir/tables_test.go | 13 + stores/dotenv/store.go | 4 +- stores/stores.go | 14 +- version/version.go | 4 +- 54 files changed, 8985 insertions(+), 197 deletions(-) create mode 100644 cmd/cmd/sops/codes/codes.go create mode 100644 cmd/cmd/sops/common/common.go create mode 100644 cmd/cmd/sops/decrypt.go create mode 100644 cmd/cmd/sops/edit.go create mode 100644 cmd/cmd/sops/encrypt.go create mode 100644 cmd/cmd/sops/formats/formats.go create mode 100644 cmd/cmd/sops/formats/formats_test.go create mode 100644 cmd/cmd/sops/main.go create mode 100644 cmd/cmd/sops/rotate.go create mode 100644 cmd/cmd/sops/set.go create mode 100644 cmd/cmd/sops/subcommand/exec/exec.go create mode 100644 cmd/cmd/sops/subcommand/exec/exec_unix.go create mode 100644 cmd/cmd/sops/subcommand/exec/exec_windows.go create mode 100644 cmd/cmd/sops/subcommand/filestatus/filestatus.go create mode 100644 cmd/cmd/sops/subcommand/filestatus/filestatus_internal_test.go create mode 100644 cmd/cmd/sops/subcommand/groups/add.go create mode 100644 cmd/cmd/sops/subcommand/groups/delete.go create mode 100644 cmd/cmd/sops/subcommand/keyservice/keyservice.go create mode 100644 cmd/cmd/sops/subcommand/publish/publish.go create mode 100644 cmd/cmd/sops/subcommand/updatekeys/updatekeys.go create mode 100644 cmd/cmd/sops/unset.go create mode 100644 decrypt/decrypt/decrypt.go create mode 100644 decrypt/decrypt/example_test.go create mode 100644 docs/docs/images/cncf-color-bg.svg create mode 100644 docs/docs/release.md create mode 100644 functional-tests/res/plainfile.yaml create mode 100644 gcpkms/gcpkms/keysource.go create mode 100644 gcpkms/gcpkms/keysource_test.go create mode 100644 gcpkms/gcpkms/mock_kms_server_test.go create mode 100644 keyservice/keyservice/client.go create mode 100644 keyservice/keyservice/keyservice.go create mode 100644 keyservice/keyservice/keyservice.pb.go create mode 100644 keyservice/keyservice/keyservice.proto create mode 100644 keyservice/keyservice/server.go create mode 100644 keyservice/keyservice/server_test.go create mode 100644 shamir/shamir/LICENSE create mode 100644 shamir/shamir/README.md create mode 100644 shamir/shamir/shamir.go create mode 100644 shamir/shamir/shamir_test.go create mode 100644 shamir/shamir/tables.go create mode 100644 shamir/shamir/tables_test.go 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 @@ +cncf-color-bg.svg \ 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) }