Sort masterkeys according to decryption-order

Co-authored-by: Gabriel Martinez <19713226+GMartinez-Sisti@users.noreply.github.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
Co-authored-by: Bastien Wermeille <bastien.wermeille@gmail.com>
Co-authored-by: Hidde Beydals <hiddeco@users.noreply.github.com>
Signed-off-by: Boris Kreitchman <bkreitch@gmail.com>
This commit is contained in:
Boris Kreitchman 2023-11-07 19:28:34 +02:00 коммит произвёл Hidde Beydals
Родитель 30281796df
Коммит c822b55290
21 изменённых файлов: 374 добавлений и 145 удалений

Просмотреть файл

@ -170,6 +170,11 @@ Given that, the only command a SOPS user needs is:
encrypted if modified, and saved back to its original location. All of these encrypted if modified, and saved back to its original location. All of these
steps, apart from the actual editing, are transparent to the user. steps, apart from the actual editing, are transparent to the user.
The order in which available decryption methods are tried can be specified with
``--decryption-order`` option or **SOPS_DECRYPTION_ORDER** environment variable
as a comma separated list. The default order is ``age,pgp``. Offline methods are
tried first and then the remaining ones.
Test with the dev PGP key Test with the dev PGP key
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~

Просмотреть файл

@ -12,8 +12,9 @@ import (
"filippo.io/age" "filippo.io/age"
"filippo.io/age/armor" "filippo.io/age/armor"
"github.com/getsops/sops/v3/logging"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/getsops/sops/v3/logging"
) )
const ( const (
@ -28,6 +29,8 @@ const (
SopsAgeKeyUserConfigPath = "sops/age/keys.txt" SopsAgeKeyUserConfigPath = "sops/age/keys.txt"
// On macOS, os.UserConfigDir() ignores XDG_CONFIG_HOME. So we handle that manually. // On macOS, os.UserConfigDir() ignores XDG_CONFIG_HOME. So we handle that manually.
xdgConfigHome = "XDG_CONFIG_HOME" xdgConfigHome = "XDG_CONFIG_HOME"
// KeyTypeIdentifier is the string used to identify an age MasterKey.
KeyTypeIdentifier = "age"
) )
// log is the global logger for any age MasterKey. // log is the global logger for any age MasterKey.
@ -225,6 +228,11 @@ func (key *MasterKey) ToMap() map[string]interface{} {
return out return out
} }
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
func getUserConfigDir() (string, error) { func getUserConfigDir() (string, error) {
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
if userConfigDir, ok := os.LookupEnv(xdgConfigHome); ok && userConfigDir != "" { if userConfigDir, ok := os.LookupEnv(xdgConfigHome); ok && userConfigDir != "" {

Просмотреть файл

@ -22,6 +22,11 @@ import (
"github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/logging"
) )
const (
// KeyTypeIdentifier is the string used to identify an Azure Key Vault MasterKey.
KeyTypeIdentifier = "azure_kv"
)
var ( var (
// log is the global logger for any Azure Key Vault MasterKey. // log is the global logger for any Azure Key Vault MasterKey.
log *logrus.Logger log *logrus.Logger
@ -215,6 +220,11 @@ func (key MasterKey) ToMap() map[string]interface{} {
return out return out
} }
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
// getTokenCredential returns the tokenCredential of the MasterKey, or // getTokenCredential returns the tokenCredential of the MasterKey, or
// azidentity.NewDefaultAzureCredential. // azidentity.NewDefaultAzureCredential.
func (key *MasterKey) getTokenCredential() (azcore.TokenCredential, error) { func (key *MasterKey) getTokenCredential() (azcore.TokenCredential, error) {

Просмотреть файл

@ -25,6 +25,7 @@ const (
NoFileSpecified int = 100 NoFileSpecified int = 100
CouldNotRetrieveKey int = 128 CouldNotRetrieveKey int = 128
NoEncryptionKeyFound int = 111 NoEncryptionKeyFound int = 111
DuplicateDecryptionKeyType int = 112
FileHasNotBeenModified int = 200 FileHasNotBeenModified int = 200
NoEditorFound int = 201 NoEditorFound int = 201
FailedToCompareVersions int = 202 FailedToCompareVersions int = 202

Просмотреть файл

@ -72,6 +72,8 @@ type DecryptTreeOpts struct {
Tree *sops.Tree Tree *sops.Tree
// KeyServices are the key services to be used for decryption of the data key // KeyServices are the key services to be used for decryption of the data key
KeyServices []keyservice.KeyServiceClient 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 is whether or not to ignore the Message Authentication Code included in the SOPS tree
IgnoreMac bool IgnoreMac bool
// Cipher is the cryptographic cipher to use to decrypt the values inside the tree // Cipher is the cryptographic cipher to use to decrypt the values inside the tree
@ -80,7 +82,7 @@ type DecryptTreeOpts struct {
// DecryptTree decrypts the tree passed in through the DecryptTreeOpts and additionally returns the decrypted data key // DecryptTree decrypts the tree passed in through the DecryptTreeOpts and additionally returns the decrypted data key
func DecryptTree(opts DecryptTreeOpts) (dataKey []byte, err error) { func DecryptTree(opts DecryptTreeOpts) (dataKey []byte, err error) {
dataKey, err = opts.Tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices) dataKey, err = opts.Tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
if err != nil { if err != nil {
return nil, NewExitError(err, codes.CouldNotRetrieveKey) return nil, NewExitError(err, codes.CouldNotRetrieveKey)
} }
@ -222,11 +224,12 @@ func GetKMSKeyWithEncryptionCtx(tree *sops.Tree) (keyGroupIndex int, keyIndex in
// GenericDecryptOpts represents decryption options and config // GenericDecryptOpts represents decryption options and config
type GenericDecryptOpts struct { type GenericDecryptOpts struct {
Cipher sops.Cipher Cipher sops.Cipher
InputStore sops.Store InputStore sops.Store
InputPath string InputPath string
IgnoreMAC bool IgnoreMAC bool
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
} }
// LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes // LoadEncryptedFileWithBugFixes is a wrapper around LoadEncryptedFile which includes

Просмотреть файл

@ -15,13 +15,14 @@ const notBinaryHint = ("This is likely not an encrypted binary file?" +
" If not, use --output-type to select the correct output type.") " If not, use --output-type to select the correct output type.")
type decryptOpts struct { type decryptOpts struct {
Cipher sops.Cipher Cipher sops.Cipher
InputStore sops.Store InputStore sops.Store
OutputStore sops.Store OutputStore sops.Store
InputPath string InputPath string
IgnoreMAC bool IgnoreMAC bool
Extract []interface{} Extract []interface{}
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
} }
func decrypt(opts decryptOpts) (decryptedFile []byte, err error) { func decrypt(opts decryptOpts) (decryptedFile []byte, err error) {
@ -37,10 +38,11 @@ func decrypt(opts decryptOpts) (decryptedFile []byte, err error) {
} }
_, err = common.DecryptTree(common.DecryptTreeOpts{ _, err = common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher, Cipher: opts.Cipher,
IgnoreMac: opts.IgnoreMAC, IgnoreMac: opts.IgnoreMAC,
Tree: tree, Tree: tree,
KeyServices: opts.KeyServices, KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

Просмотреть файл

@ -20,13 +20,14 @@ import (
) )
type editOpts struct { type editOpts struct {
Cipher sops.Cipher Cipher sops.Cipher
InputStore common.Store InputStore common.Store
OutputStore common.Store OutputStore common.Store
InputPath string InputPath string
IgnoreMAC bool IgnoreMAC bool
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
ShowMasterKeys bool DecryptionOrder []string
ShowMasterKeys bool
} }
type editExampleOpts struct { type editExampleOpts struct {
@ -96,7 +97,11 @@ func edit(opts editOpts) ([]byte, error) {
} }
// Decrypt the file // Decrypt the file
dataKey, err := common.DecryptTree(common.DecryptTreeOpts{ dataKey, err := common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher, IgnoreMac: opts.IgnoreMAC, Tree: tree, KeyServices: opts.KeyServices, Cipher: opts.Cipher,
IgnoreMac: opts.IgnoreMAC,
Tree: tree,
KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

Просмотреть файл

@ -1,4 +1,4 @@
package main //import "github.com/getsops/sops/v3/cmd/sops" package main // import "github.com/getsops/sops/v3/cmd/sops"
import ( import (
"context" "context"
@ -13,6 +13,11 @@ import (
"strconv" "strconv"
"strings" "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"
"github.com/getsops/sops/v3/aes" "github.com/getsops/sops/v3/aes"
"github.com/getsops/sops/v3/age" "github.com/getsops/sops/v3/age"
@ -36,10 +41,6 @@ import (
"github.com/getsops/sops/v3/stores/dotenv" "github.com/getsops/sops/v3/stores/dotenv"
"github.com/getsops/sops/v3/stores/json" "github.com/getsops/sops/v3/stores/json"
"github.com/getsops/sops/v3/version" "github.com/getsops/sops/v3/version"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
) )
var log *logrus.Logger var log *logrus.Logger
@ -172,13 +173,19 @@ func main() {
inputStore := inputStore(c, fileName) inputStore := inputStore(c, fileName)
svcs := keyservices(c) svcs := keyservices(c)
order, err := decryptionOrder(c.String("decryption-order"))
if err != nil {
return toExitError(err)
}
opts := decryptOpts{ opts := decryptOpts{
OutputStore: &dotenv.Store{}, OutputStore: &dotenv.Store{},
InputStore: inputStore, InputStore: inputStore,
InputPath: fileName, InputPath: fileName,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
KeyServices: svcs, KeyServices: svcs,
IgnoreMAC: c.Bool("ignore-mac"), DecryptionOrder: order,
IgnoreMAC: c.Bool("ignore-mac"),
} }
output, err := decrypt(opts) output, err := decrypt(opts)
@ -241,13 +248,19 @@ func main() {
outputStore := outputStore(c, fileName) outputStore := outputStore(c, fileName)
svcs := keyservices(c) svcs := keyservices(c)
order, err := decryptionOrder(c.String("decryption-order"))
if err != nil {
return toExitError(err)
}
opts := decryptOpts{ opts := decryptOpts{
OutputStore: outputStore, OutputStore: outputStore,
InputStore: inputStore, InputStore: inputStore,
InputPath: fileName, InputPath: fileName,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
KeyServices: svcs, KeyServices: svcs,
IgnoreMAC: c.Bool("ignore-mac"), DecryptionOrder: order,
IgnoreMAC: c.Bool("ignore-mac"),
} }
output, err := decrypt(opts) output, err := decrypt(opts)
@ -316,21 +329,25 @@ func main() {
if info.IsDir() && !c.Bool("recursive") { if info.IsDir() && !c.Bool("recursive") {
return fmt.Errorf("can't operate on a directory without --recursive flag.") 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 { err = filepath.Walk(path, func(subPath string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return toExitError(err) return toExitError(err)
} }
if !info.IsDir() { if !info.IsDir() {
err = publishcmd.Run(publishcmd.Opts{ err = publishcmd.Run(publishcmd.Opts{
ConfigPath: configPath, ConfigPath: configPath,
InputPath: subPath, InputPath: subPath,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
KeyServices: keyservices(c), KeyServices: keyservices(c),
InputStore: inputStore(c, subPath), DecryptionOrder: order,
Interactive: !c.Bool("yes"), InputStore: inputStore(c, subPath),
OmitExtensions: c.Bool("omit-extensions"), Interactive: !c.Bool("yes"),
Recursive: c.Bool("recursive"), OmitExtensions: c.Bool("omit-extensions"),
RootPath: path, Recursive: c.Bool("recursive"),
}) })
if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil { if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
return cliErr return cliErr
@ -773,6 +790,11 @@ func main() {
Name: "filename-override", Name: "filename-override",
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type", 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...) }, keyserviceFlags...)
app.Action = func(c *cli.Context) error { app.Action = func(c *cli.Context) error {
@ -859,6 +881,10 @@ func main() {
outputStore := outputStore(c, fileNameOverride) outputStore := outputStore(c, fileNameOverride)
svcs := keyservices(c) svcs := keyservices(c)
order, err := decryptionOrder(c.String("decryption-order"))
if err != nil {
return toExitError(err)
}
var output []byte var output []byte
if c.Bool("encrypt") { if c.Bool("encrypt") {
var groups []sops.KeyGroup var groups []sops.KeyGroup
@ -894,13 +920,14 @@ func main() {
return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat) return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
} }
output, err = decrypt(decryptOpts{ output, err = decrypt(decryptOpts{
OutputStore: outputStore, OutputStore: outputStore,
InputStore: inputStore, InputStore: inputStore,
InputPath: fileName, InputPath: fileName,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
Extract: extract, Extract: extract,
KeyServices: svcs, KeyServices: svcs,
IgnoreMAC: c.Bool("ignore-mac"), DecryptionOrder: order,
IgnoreMAC: c.Bool("ignore-mac"),
}) })
} }
if c.Bool("rotate") { if c.Bool("rotate") {
@ -975,6 +1002,7 @@ func main() {
InputPath: fileName, InputPath: fileName,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
KeyServices: svcs, KeyServices: svcs,
DecryptionOrder: order,
IgnoreMAC: c.Bool("ignore-mac"), IgnoreMAC: c.Bool("ignore-mac"),
AddMasterKeys: addMasterKeys, AddMasterKeys: addMasterKeys,
RemoveMasterKeys: rmMasterKeys, RemoveMasterKeys: rmMasterKeys,
@ -994,14 +1022,15 @@ func main() {
return toExitError(err) return toExitError(err)
} }
output, err = set(setOpts{ output, err = set(setOpts{
OutputStore: outputStore, OutputStore: outputStore,
InputStore: inputStore, InputStore: inputStore,
InputPath: fileName, InputPath: fileName,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
KeyServices: svcs, KeyServices: svcs,
IgnoreMAC: c.Bool("ignore-mac"), DecryptionOrder: order,
Value: value, IgnoreMAC: c.Bool("ignore-mac"),
TreePath: path, Value: value,
TreePath: path,
}) })
} }
@ -1010,13 +1039,14 @@ func main() {
_, statErr := os.Stat(fileName) _, statErr := os.Stat(fileName)
fileExists := statErr == nil fileExists := statErr == nil
opts := editOpts{ opts := editOpts{
OutputStore: outputStore, OutputStore: outputStore,
InputStore: inputStore, InputStore: inputStore,
InputPath: fileName, InputPath: fileName,
Cipher: aes.NewCipher(), Cipher: aes.NewCipher(),
KeyServices: svcs, KeyServices: svcs,
IgnoreMAC: c.Bool("ignore-mac"), DecryptionOrder: order,
ShowMasterKeys: c.Bool("show-master-keys"), IgnoreMAC: c.Bool("ignore-mac"),
ShowMasterKeys: c.Bool("show-master-keys"),
} }
if fileExists { if fileExists {
output, err = edit(opts) output, err = edit(opts)
@ -1351,3 +1381,18 @@ func extractSetArguments(set string) (path []interface{}, valueToInsert interfac
} }
return path, valueToInsert, nil 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
}

Просмотреть файл

@ -20,15 +20,17 @@ type rotateOpts struct {
AddMasterKeys []keys.MasterKey AddMasterKeys []keys.MasterKey
RemoveMasterKeys []keys.MasterKey RemoveMasterKeys []keys.MasterKey
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
} }
func rotate(opts rotateOpts) ([]byte, error) { func rotate(opts rotateOpts) ([]byte, error) {
tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{ tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
Cipher: opts.Cipher, Cipher: opts.Cipher,
InputStore: opts.InputStore, InputStore: opts.InputStore,
InputPath: opts.InputPath, InputPath: opts.InputPath,
IgnoreMAC: opts.IgnoreMAC, IgnoreMAC: opts.IgnoreMAC,
KeyServices: opts.KeyServices, KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -39,8 +41,11 @@ func rotate(opts rotateOpts) ([]byte, error) {
}) })
_, err = common.DecryptTree(common.DecryptTreeOpts{ _, err = common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher, IgnoreMac: opts.IgnoreMAC, Tree: tree, Cipher: opts.Cipher,
KeyServices: opts.KeyServices, IgnoreMac: opts.IgnoreMAC,
Tree: tree,
KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

Просмотреть файл

@ -10,14 +10,15 @@ import (
) )
type setOpts struct { type setOpts struct {
Cipher sops.Cipher Cipher sops.Cipher
InputStore sops.Store InputStore sops.Store
OutputStore sops.Store OutputStore sops.Store
InputPath string InputPath string
IgnoreMAC bool IgnoreMAC bool
TreePath []interface{} TreePath []interface{}
Value interface{} Value interface{}
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
} }
func set(opts setOpts) ([]byte, error) { func set(opts setOpts) ([]byte, error) {
@ -36,10 +37,11 @@ func set(opts setOpts) ([]byte, error) {
// Decrypt the file // Decrypt the file
dataKey, err := common.DecryptTree(common.DecryptTreeOpts{ dataKey, err := common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher, Cipher: opts.Cipher,
IgnoreMac: opts.IgnoreMAC, IgnoreMac: opts.IgnoreMAC,
Tree: tree, Tree: tree,
KeyServices: opts.KeyServices, KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

Просмотреть файл

@ -10,13 +10,14 @@ import (
// AddOpts are the options for adding a key group to a SOPS file // AddOpts are the options for adding a key group to a SOPS file
type AddOpts struct { type AddOpts struct {
InputPath string InputPath string
InputStore sops.Store InputStore sops.Store
OutputStore sops.Store OutputStore sops.Store
Group sops.KeyGroup Group sops.KeyGroup
GroupThreshold int GroupThreshold int
InPlace bool InPlace bool
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
} }
// Add adds a key group to a SOPS file // Add adds a key group to a SOPS file
@ -25,7 +26,7 @@ func Add(opts AddOpts) error {
if err != nil { if err != nil {
return err return err
} }
dataKey, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices) dataKey, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
if err != nil { if err != nil {
return err return err
} }

Просмотреть файл

@ -12,13 +12,14 @@ import (
// DeleteOpts are the options for deleting a key group from a SOPS file // DeleteOpts are the options for deleting a key group from a SOPS file
type DeleteOpts struct { type DeleteOpts struct {
InputPath string InputPath string
InputStore sops.Store InputStore sops.Store
OutputStore sops.Store OutputStore sops.Store
Group uint Group uint
GroupThreshold int GroupThreshold int
InPlace bool InPlace bool
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
DecryptionOrder []string
} }
// Delete deletes a key group from a SOPS file // Delete deletes a key group from a SOPS file
@ -27,7 +28,7 @@ func Delete(opts DeleteOpts) error {
if err != nil { if err != nil {
return err return err
} }
dataKey, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices) dataKey, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
if err != nil { if err != nil {
return err return err
} }

Просмотреть файл

@ -27,15 +27,16 @@ func init() {
// Opts represents publish options and config // Opts represents publish options and config
type Opts struct { type Opts struct {
Interactive bool Interactive bool
Cipher sops.Cipher Cipher sops.Cipher
ConfigPath string ConfigPath string
InputPath string InputPath string
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
InputStore sops.Store DecryptionOrder []string
OmitExtensions bool InputStore sops.Store
Recursive bool OmitExtensions bool
RootPath string Recursive bool
RootPath string
} }
// Run publish operation // Run publish operation
@ -81,10 +82,11 @@ func Run(opts Opts) error {
if len(conf.KeyGroups[0]) != 0 { if len(conf.KeyGroups[0]) != 0 {
log.Debug("Re-encrypting tree before publishing") log.Debug("Re-encrypting tree before publishing")
_, err = common.DecryptTree(common.DecryptTreeOpts{ _, err = common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher, Cipher: opts.Cipher,
IgnoreMac: false, IgnoreMac: false,
Tree: tree, Tree: tree,
KeyServices: opts.KeyServices, KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return err return err
@ -137,10 +139,11 @@ func Run(opts Opts) error {
} }
case *publish.VaultDestination: case *publish.VaultDestination:
_, err = common.DecryptTree(common.DecryptTreeOpts{ _, err = common.DecryptTree(common.DecryptTreeOpts{
Cipher: opts.Cipher, Cipher: opts.Cipher,
IgnoreMac: false, IgnoreMac: false,
Tree: tree, Tree: tree,
KeyServices: opts.KeyServices, KeyServices: opts.KeyServices,
DecryptionOrder: opts.DecryptionOrder,
}) })
if err != nil { if err != nil {
return err return err

Просмотреть файл

@ -14,12 +14,13 @@ import (
// Opts represents key operation options and config // Opts represents key operation options and config
type Opts struct { type Opts struct {
InputPath string InputPath string
GroupQuorum int GroupQuorum int
KeyServices []keyservice.KeyServiceClient KeyServices []keyservice.KeyServiceClient
Interactive bool DecryptionOrder []string
ConfigPath string Interactive bool
InputType string ConfigPath string
InputType string
} }
// UpdateKeys update the keys for a given file // UpdateKeys update the keys for a given file
@ -83,7 +84,7 @@ func updateFile(opts Opts) error {
return nil return nil
} }
} }
key, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices) key, err := tree.Metadata.GetDataKeyWithKeyServices(opts.KeyServices, opts.DecryptionOrder)
if err != nil { if err != nil {
return common.NewExitError(err, codes.CouldNotRetrieveKey) return common.NewExitError(err, codes.CouldNotRetrieveKey)
} }

Просмотреть файл

@ -23,6 +23,8 @@ const (
// a path to a credentials file, or directly as the variable's value in JSON // a path to a credentials file, or directly as the variable's value in JSON
// format. // format.
SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS" SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS"
// KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey.
KeyTypeIdentifier = "gcp_kms"
) )
var ( var (
@ -196,6 +198,11 @@ func (key MasterKey) ToMap() map[string]interface{} {
return out 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 // newKMSClient returns a GCP KMS client configured with the credentialJSON
// and/or grpcConn, falling back to environmental defaults. // and/or grpcConn, falling back to environmental defaults.
// It returns an error if the ResourceID is invalid, or if the setup of the // It returns an error if the ResourceID is invalid, or if the setup of the

Просмотреть файл

@ -21,6 +21,11 @@ import (
"github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/logging"
) )
const (
// KeyTypeIdentifier is the string used to identify a Vault MasterKey.
KeyTypeIdentifier = "hc_vault"
)
func init() { func init() {
log = logging.NewLogger("VAULT_TRANSIT") log = logging.NewLogger("VAULT_TRANSIT")
} }
@ -216,6 +221,11 @@ func (key MasterKey) ToMap() map[string]interface{} {
return out return out
} }
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
// encryptPath returns the path for Encrypt requests. // encryptPath returns the path for Encrypt requests.
func (key *MasterKey) encryptPath() string { func (key *MasterKey) encryptPath() string {
return path.Join(key.EnginePath, "encrypt", key.KeyName) return path.Join(key.EnginePath, "encrypt", key.KeyName)

Просмотреть файл

@ -10,4 +10,5 @@ type MasterKey interface {
NeedsRotation() bool NeedsRotation() bool
ToString() string ToString() string
ToMap() map[string]interface{} ToMap() map[string]interface{}
TypeToIdentifier() string
} }

Просмотреть файл

@ -3,7 +3,7 @@ Package kms contains an implementation of the github.com/getsops/sops/v3.MasterK
interface that encrypts and decrypts the data key using AWS KMS with the SDK interface that encrypts and decrypts the data key using AWS KMS with the SDK
for Go V2. for Go V2.
*/ */
package kms //import "github.com/getsops/sops/v3/kms" package kms // import "github.com/getsops/sops/v3/kms"
import ( import (
"context" "context"
@ -19,8 +19,9 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/getsops/sops/v3/logging"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/getsops/sops/v3/logging"
) )
const ( const (
@ -34,6 +35,8 @@ const (
roleSessionNameLengthLimit = 64 roleSessionNameLengthLimit = 64
// kmsTTL is the duration after which a MasterKey requires rotation. // kmsTTL is the duration after which a MasterKey requires rotation.
kmsTTL = time.Hour * 24 * 30 * 6 kmsTTL = time.Hour * 24 * 30 * 6
// KeyTypeIdentifier is the string used to identify an AWS KMS MasterKey.
KeyTypeIdentifier = "kms"
) )
var ( var (
@ -297,6 +300,11 @@ func (key MasterKey) ToMap() map[string]interface{} {
return out return out
} }
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
// createKMSConfig returns an AWS config with the credentialsProvider of the // createKMSConfig returns an AWS config with the credentialsProvider of the
// MasterKey, or the default configuration sources. // MasterKey, or the default configuration sources.
func (key MasterKey) createKMSConfig() (*aws.Config, error) { func (key MasterKey) createKMSConfig() (*aws.Config, error) {

Просмотреть файл

@ -4,7 +4,7 @@ interface that encrypts and decrypts the data key by first trying with the
github.com/ProtonMail/go-crypto/openpgp package and if that fails, by calling github.com/ProtonMail/go-crypto/openpgp package and if that fails, by calling
the "gpg" binary. the "gpg" binary.
*/ */
package pgp //import "github.com/getsops/sops/v3/pgp" package pgp // import "github.com/getsops/sops/v3/pgp"
import ( import (
"bytes" "bytes"
@ -22,12 +22,15 @@ import (
"github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/armor"
gpgagent "github.com/getsops/gopgagent" gpgagent "github.com/getsops/gopgagent"
"github.com/getsops/sops/v3/logging"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/term" "golang.org/x/term"
"github.com/getsops/sops/v3/logging"
) )
const ( const (
// KeyTypeIdentifier is the string used to identify a PGP MasterKey.
KeyTypeIdentifier = "pgp"
// SopsGpgExecEnv can be set as an environment variable to overwrite the // SopsGpgExecEnv can be set as an environment variable to overwrite the
// GnuPG binary used. // GnuPG binary used.
SopsGpgExecEnv = "SOPS_GPG_EXEC" SopsGpgExecEnv = "SOPS_GPG_EXEC"
@ -449,6 +452,11 @@ func (key MasterKey) ToMap() map[string]interface{} {
return out return out
} }
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
// retrievePubKey attempts to retrieve the public key from the public keyring // retrievePubKey attempts to retrieve the public key from the public keyring
// by Fingerprint. // by Fingerprint.
func (key *MasterKey) retrievePubKey() (openpgp.Entity, error) { func (key *MasterKey) retrievePubKey() (openpgp.Entity, error) {

57
sops.go
Просмотреть файл

@ -34,7 +34,7 @@ be recalculated and compared with the MAC stored in the document to verify that
fraudulent changes have been applied. The MAC covers keys and values as well as their fraudulent changes have been applied. The MAC covers keys and values as well as their
ordering. ordering.
*/ */
package sops //import "github.com/getsops/sops/v3" package sops // import "github.com/getsops/sops/v3"
import ( import (
"crypto/rand" "crypto/rand"
@ -42,22 +42,28 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/audit" "github.com/getsops/sops/v3/audit"
"github.com/getsops/sops/v3/keys" "github.com/getsops/sops/v3/keys"
"github.com/getsops/sops/v3/keyservice" "github.com/getsops/sops/v3/keyservice"
"github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/logging"
"github.com/getsops/sops/v3/pgp"
"github.com/getsops/sops/v3/shamir" "github.com/getsops/sops/v3/shamir"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
) )
// DefaultUnencryptedSuffix is the default suffix a TreeItem key has to end with for sops to leave its Value unencrypted // DefaultUnencryptedSuffix is the default suffix a TreeItem key has to end with for sops to leave its Value unencrypted
const DefaultUnencryptedSuffix = "_unencrypted" const DefaultUnencryptedSuffix = "_unencrypted"
var DefaultDecryptionOrder = []string{age.KeyTypeIdentifier, pgp.KeyTypeIdentifier}
type sopsError string type sopsError string
func (e sopsError) Error() string { func (e sopsError) Error() string {
@ -648,7 +654,7 @@ func (m *Metadata) UpdateMasterKeys(dataKey []byte) (errs []error) {
// GetDataKeyWithKeyServices retrieves the data key, asking KeyServices to decrypt it with each // GetDataKeyWithKeyServices retrieves the data key, asking KeyServices to decrypt it with each
// MasterKey in the Metadata's KeySources until one of them succeeds. // MasterKey in the Metadata's KeySources until one of them succeeds.
func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient) ([]byte, error) { func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient, decryptionOrder []string) ([]byte, error) {
if m.DataKey != nil { if m.DataKey != nil {
return m.DataKey, nil return m.DataKey, nil
} }
@ -658,7 +664,7 @@ func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient)
} }
var parts [][]byte var parts [][]byte
for i, group := range m.KeyGroups { for i, group := range m.KeyGroups {
part, err := decryptKeyGroup(group, svcs) part, err := decryptKeyGroup(group, svcs, decryptionOrder)
if err == nil { if err == nil {
parts = append(parts, part) parts = append(parts, part)
} }
@ -688,9 +694,13 @@ func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient)
// decryptKeyGroup tries to decrypt the contents of the provided KeyGroup with // decryptKeyGroup tries to decrypt the contents of the provided KeyGroup with
// any of the MasterKeys in the KeyGroup with any of the provided key services, // any of the MasterKeys in the KeyGroup with any of the provided key services,
// returning as soon as one key service succeeds. // returning as soon as one key service succeeds.
func decryptKeyGroup(group KeyGroup, svcs []keyservice.KeyServiceClient) ([]byte, error) { func decryptKeyGroup(group KeyGroup, svcs []keyservice.KeyServiceClient, decryptionOrder []string) ([]byte, error) {
var keyErrs []error var keyErrs []error
for _, key := range group { // Sort MasterKeys in the group so we try them in specific order
// Use sorted indices to avoid group slice modification
indices := sortKeyGroupIndices(group, decryptionOrder)
for _, indexVal := range indices {
key := group[indexVal]
part, err := decryptKey(key, svcs) part, err := decryptKey(key, svcs)
if err != nil { if err != nil {
keyErrs = append(keyErrs, err) keyErrs = append(keyErrs, err)
@ -701,6 +711,37 @@ func decryptKeyGroup(group KeyGroup, svcs []keyservice.KeyServiceClient) ([]byte
return nil, decryptKeyErrors(keyErrs) return nil, decryptKeyErrors(keyErrs)
} }
// sortKeyGroupIndices returns indices that would sort the KeyGroup
// according to decryptionOrder
func sortKeyGroupIndices(group KeyGroup, decryptionOrder []string) []int {
priorities := make(map[string]int)
// give ordered weights
for i, v := range decryptionOrder {
priorities[v] = i
}
maxPriority := len(decryptionOrder)
// initialize indices
n := len(group)
indices := make([]int, n)
for i := 0; i < n; i++ {
indices[i] = i
}
sort.SliceStable(indices, func(i, j int) bool {
keyTypeI := group[indices[i]].TypeToIdentifier()
keyTypeJ := group[indices[j]].TypeToIdentifier()
priorityI, ok := priorities[keyTypeI]
if !ok {
priorityI = maxPriority
}
priorityJ, ok := priorities[keyTypeJ]
if !ok {
priorityJ = maxPriority
}
return priorityI < priorityJ
})
return indices
}
// decryptKey tries to decrypt the contents of the provided MasterKey with any // decryptKey tries to decrypt the contents of the provided MasterKey with any
// of the key services, returning as soon as one key service succeeds. // of the key services, returning as soon as one key service succeeds.
func decryptKey(key keys.MasterKey, svcs []keyservice.KeyServiceClient) ([]byte, error) { func decryptKey(key keys.MasterKey, svcs []keyservice.KeyServiceClient) ([]byte, error) {
@ -739,7 +780,7 @@ func decryptKey(key keys.MasterKey, svcs []keyservice.KeyServiceClient) ([]byte,
func (m Metadata) GetDataKey() ([]byte, error) { func (m Metadata) GetDataKey() ([]byte, error) {
return m.GetDataKeyWithKeyServices([]keyservice.KeyServiceClient{ return m.GetDataKeyWithKeyServices([]keyservice.KeyServiceClient{
keyservice.NewLocalClient(), keyservice.NewLocalClient(),
}) }, nil)
} }
// ToBytes converts a string, int, float or bool to a byte representation. // ToBytes converts a string, int, float or bool to a byte representation.

Просмотреть файл

@ -7,6 +7,10 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/hcvault"
"github.com/getsops/sops/v3/pgp"
) )
type reverseCipher struct{} type reverseCipher struct{}
@ -842,3 +846,61 @@ func TestEmitAsMap(t *testing.T) {
assert.Equal(t, expected, data) assert.Equal(t, expected, data)
} }
} }
func TestSortKeyGroupIndices(t *testing.T) {
t.Run("default order", func(t *testing.T) {
group := KeyGroup{&hcvault.MasterKey{}, &age.MasterKey{}, &pgp.MasterKey{}}
expected := []int{1, 2, 0}
indices := sortKeyGroupIndices(group, DefaultDecryptionOrder)
assert.Equal(t, expected, indices)
})
t.Run("different keygroup", func(t *testing.T) {
group := KeyGroup{&hcvault.MasterKey{}, &pgp.MasterKey{}, &age.MasterKey{}}
expected := []int{2, 1, 0}
indices := sortKeyGroupIndices(group, DefaultDecryptionOrder)
assert.Equal(t, expected, indices)
})
t.Run("repeated key", func(t *testing.T) {
group := KeyGroup{&pgp.MasterKey{}, &hcvault.MasterKey{}, &pgp.MasterKey{}, &age.MasterKey{}}
expected := []int{3, 0, 2, 1}
indices := sortKeyGroupIndices(group, DefaultDecryptionOrder)
assert.Equal(t, expected, indices)
})
t.Run("full order", func(t *testing.T) {
group := KeyGroup{&hcvault.MasterKey{}, &pgp.MasterKey{}, &age.MasterKey{}}
expected := []int{1, 2, 0}
indices := sortKeyGroupIndices(group, []string{"pgp", "age", "hc_vault"})
assert.Equal(t, expected, indices)
})
t.Run("empty order", func(t *testing.T) {
group := KeyGroup{&hcvault.MasterKey{}, &pgp.MasterKey{}, &age.MasterKey{}}
expected := []int{0, 1, 2}
indices := sortKeyGroupIndices(group, []string{})
assert.Equal(t, expected, indices)
})
t.Run("one match", func(t *testing.T) {
group := KeyGroup{&hcvault.MasterKey{}, &pgp.MasterKey{}, &age.MasterKey{}}
expected := []int{2, 0, 1}
indices := sortKeyGroupIndices(group, []string{"azure_kv", "age"})
assert.Equal(t, expected, indices)
})
t.Run("nonmatching order", func(t *testing.T) {
group := KeyGroup{&pgp.MasterKey{}, &hcvault.MasterKey{}, &age.MasterKey{}}
expected := []int{0, 1, 2}
indices := sortKeyGroupIndices(group, []string{"azure_kv"})
assert.Equal(t, expected, indices)
})
t.Run("nonexistent keys", func(t *testing.T) {
group := KeyGroup{&hcvault.MasterKey{}, &pgp.MasterKey{}, &age.MasterKey{}}
expected := []int{2, 1, 0}
indices := sortKeyGroupIndices(group, []string{"dummy1", "age", "dummy2", "pgp", "dummy3"})
assert.Equal(t, expected, indices)
})
}