diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 4b8c09042..f574155d2 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -235,20 +235,35 @@ func encrypt(c *cli.Context, file string, fileBytes []byte, output io.Writer) er metadata.UnencryptedSuffix = c.String("unencrypted-suffix") metadata.Version = "2.0.0" var kmsKeys []sops.MasterKey + var pgpKeys []sops.MasterKey + if c.String("kms") != "" { for _, k := range kms.MasterKeysFromArnString(c.String("kms")) { kmsKeys = append(kmsKeys, &k) } } - metadata.KeySources = append(metadata.KeySources, sops.KeySource{Name: "kms", Keys: kmsKeys}) - - var pgpKeys []sops.MasterKey if c.String("pgp") != "" { for _, k := range pgp.MasterKeysFromFingerprintString(c.String("pgp")) { pgpKeys = append(pgpKeys, &k) } } - metadata.KeySources = append(metadata.KeySources, sops.KeySource{Name: "pgp", Keys: pgpKeys}) + + if c.String("kms") == "" && c.String("pgp") == "" { + kmsString, pgpString, err := yaml.MasterKeyStringsForFile(file, nil) + if err == nil { + for _, k := range pgp.MasterKeysFromFingerprintString(pgpString) { + pgpKeys = append(pgpKeys, &k) + } + for _, k := range kms.MasterKeysFromArnString(kmsString) { + kmsKeys = append(kmsKeys, &k) + } + } + } + kmsKs := sops.KeySource{Name: "kms", Keys: kmsKeys} + pgpKs := sops.KeySource{Name: "pgp", Keys: pgpKeys} + metadata.KeySources = append(metadata.KeySources, kmsKs) + metadata.KeySources = append(metadata.KeySources, pgpKs) + key := make([]byte, 32) _, err = rand.Read(key) if err != nil { diff --git a/yaml/config.go b/yaml/config.go new file mode 100644 index 000000000..300eb94ec --- /dev/null +++ b/yaml/config.go @@ -0,0 +1,87 @@ +package yaml + +import ( + "fmt" + "github.com/autrilla/yaml" + "io/ioutil" + "os" + "path" + "regexp" +) + +type fileSystem interface { + Stat(name string) (os.FileInfo, error) +} + +type osFS struct { + stat func(string) (os.FileInfo, error) +} + +func (fs osFS) Stat(name string) (os.FileInfo, error) { + return fs.stat(name) +} + +var fs fileSystem = osFS{stat: os.Stat} + +const ( + maxDepth = 100 + configFileName = ".sops.yaml" +) + +// FindConfigFile looks for a sops config file in the current working directory and on parent directories, up to the limit defined by the maxDepth constant. +func FindConfigFile(start string) (string, error) { + filepath := path.Dir(start) + for i := 0; i < maxDepth; i++ { + _, err := fs.Stat(path.Join(filepath, configFileName)) + if err != nil { + filepath = path.Join(filepath, "..") + } else { + return path.Join(filepath, configFileName), nil + } + } + return "", fmt.Errorf("Config file not found") +} + +type configFile struct { + CreationRules []creationRule `yaml:"creation_rules"` +} + +type creationRule struct { + FilenameRegex string `yaml:"filename_regex"` + KMS string + PGP string +} + +// Load loads a sops config file into a temporary struct +func (f *configFile) load(bytes []byte) error { + err := yaml.Unmarshal(bytes, f) + if err != nil { + return fmt.Errorf("Could not unmarshal config file: %s", err) + } + return nil +} + +// MasterKeyStringsForFile returns a comma separated string of KMS ARNs and a comma separated list of PGP fingerprints. If the config bytes are left empty, the function will look for the config file by itself. +func MasterKeyStringsForFile(filepath string, confBytes []byte) (kms, pgp string, err error) { + if confBytes == nil { + confPath, err := FindConfigFile(filepath) + if err != nil { + return "", "", err + } + confBytes, err = ioutil.ReadFile(confPath) + } + if err != nil { + return "", "", fmt.Errorf("Could not read config file: %s", err) + } + conf := configFile{} + err = conf.load(confBytes) + if err != nil { + return "", "", fmt.Errorf("Error loading config: %s", err) + } + for _, rule := range conf.CreationRules { + if match, _ := regexp.MatchString(rule.FilenameRegex, filepath); match { + return rule.KMS, rule.PGP, nil + } + } + return "", "", nil +} diff --git a/yaml/config_test.go b/yaml/config_test.go new file mode 100644 index 000000000..afd3dd9b1 --- /dev/null +++ b/yaml/config_test.go @@ -0,0 +1,85 @@ +package yaml + +import ( + "github.com/stretchr/testify/assert" + "os" + "path" + "testing" +) + +type mockFS struct { + stat func(string) (os.FileInfo, error) +} + +func (fs mockFS) Stat(name string) (os.FileInfo, error) { + return fs.stat(name) +} + +func TestFindConfigFileRecursive(t *testing.T) { + expectedPath := path.Clean("./../../.sops.yaml") + fs = mockFS{stat: func(name string) (os.FileInfo, error) { + if name == expectedPath { + return nil, nil + } + return nil, &os.PathError{} + }} + filepath, err := FindConfigFile(".") + assert.Equal(t, nil, err) + assert.Equal(t, expectedPath, filepath) +} + +func TestFindConfigFileCurrentDir(t *testing.T) { + expectedPath := path.Clean(".sops.yaml") + fs = mockFS{stat: func(name string) (os.FileInfo, error) { + if name == expectedPath { + return nil, nil + } + return nil, &os.PathError{} + }} + filepath, err := FindConfigFile(".") + assert.Equal(t, nil, err) + assert.Equal(t, expectedPath, filepath) +} + +var sampleConfig = []byte(` +creation_rules: + - filename_regex: foobar* + kms: "1" + pgp: "2" + - filename_regex: "" + kms: foo + pgp: bar +`) + +func TestLoadConfigFile(t *testing.T) { + expected := configFile{ + CreationRules: []creationRule{ + creationRule{ + FilenameRegex: "foobar*", + KMS: "1", + PGP: "2", + }, + creationRule{ + FilenameRegex: "", + KMS: "foo", + PGP: "bar", + }, + }, + } + + conf := configFile{} + err := conf.load(sampleConfig) + assert.Equal(t, nil, err) + assert.Equal(t, expected, conf) +} + +func TestMasterKeyStringsForFile(t *testing.T) { + kms, pgp, err := MasterKeyStringsForFile("foobar2000", sampleConfig) + assert.Equal(t, nil, err) + assert.Equal(t, "1", kms) + assert.Equal(t, "2", pgp) + kms, pgp, err = MasterKeyStringsForFile("whatever", sampleConfig) + assert.Equal(t, nil, err) + assert.Equal(t, "foo", kms) + assert.Equal(t, "bar", pgp) +}