diff --git a/cmd/sops/common/common.go b/cmd/sops/common/common.go index 0bbd51e5f..b4dac243a 100644 --- a/cmd/sops/common/common.go +++ b/cmd/sops/common/common.go @@ -12,6 +12,7 @@ import ( "go.mozilla.org/sops/keyservice" "go.mozilla.org/sops/stores/json" "go.mozilla.org/sops/stores/yaml" + "go.mozilla.org/sops/stores/env" "gopkg.in/urfave/cli.v1" ) @@ -104,11 +105,17 @@ func IsJSONFile(path string) bool { return strings.HasSuffix(path, ".json") } +func IsEnvFile(path string) bool { + return strings.HasSuffix(path, ".env") +} + func DefaultStoreForPath(path string) sops.Store { if IsYAMLFile(path) { return &yaml.Store{} } else if IsJSONFile(path) { return &json.Store{} + } else if IsEnvFile(path) { + return &env.Store{} } return &json.BinaryStore{} } diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 42010854c..45b91dbf4 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -36,6 +36,7 @@ import ( "go.mozilla.org/sops/pgp" "go.mozilla.org/sops/stores/json" yamlstores "go.mozilla.org/sops/stores/yaml" + "go.mozilla.org/sops/stores/env" "gopkg.in/urfave/cli.v1" ) @@ -675,6 +676,8 @@ func inputStore(context *cli.Context, path string) sops.Store { return &yamlstores.Store{} case "json": return &json.Store{} + case "env": + return &env.Store{} case "binary": return &json.BinaryStore{} default: @@ -688,6 +691,8 @@ func outputStore(context *cli.Context, path string) sops.Store { return &yamlstores.Store{} case "json": return &json.Store{} + case "env": + return &env.Store{} case "binary": return &json.BinaryStore{} default: diff --git a/decrypt/decrypt.go b/decrypt/decrypt.go index 96a796bba..a85b4b2bb 100644 --- a/decrypt/decrypt.go +++ b/decrypt/decrypt.go @@ -13,6 +13,7 @@ import ( "go.mozilla.org/sops/aes" sopsjson "go.mozilla.org/sops/stores/json" sopsyaml "go.mozilla.org/sops/stores/yaml" + sopsenv "go.mozilla.org/sops/stores/env" ) // File is a wrapper around Data that reads a local encrypted @@ -38,6 +39,8 @@ func Data(data []byte, format string) (cleartext []byte, err error) { store = &sopsjson.Store{} case "yaml": store = &sopsyaml.Store{} + case "env": + store = &sopsenv.Store{} default: store = &sopsjson.BinaryStore{} } diff --git a/stores/env/store.go b/stores/env/store.go new file mode 100644 index 000000000..4b594b8ff --- /dev/null +++ b/stores/env/store.go @@ -0,0 +1,119 @@ +package env //import "go.mozilla.org/sops/stores/env" +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/gob" + "fmt" + "go.mozilla.org/sops" + "go.mozilla.org/sops/stores" + "strings" +) + +// Store handles storage of env data +type Store struct { +} + +func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { + branch, err := store.LoadPlainFile(in) + if err != nil { return sops.Tree{}, err } + + storeMetadata := stores.Metadata{} + index := -1 + for i, item := range branch { + if item.Key == "_metadata" { + storeMetadata, err = FromGOB64(fmt.Sprint(item.Value)) + if err != nil { return sops.Tree{}, err } + index = i + break + } + } + if index == -1 { return sops.Tree{}, sops.MetadataNotFound } + + internalMetadata, err := storeMetadata.ToInternal() + if err != nil { return sops.Tree{}, err } + + return sops.Tree{ + Branch: append(branch[:index], branch[index+1:]...), + Metadata: internalMetadata, + }, nil +} + +func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranch, error) { + branch := make(sops.TreeBranch, 0) + reader := bytes.NewReader(in) + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + if line == "" { continue } + pos := strings.Index(line, "=") + if pos == -1 { + // FIXME: Print line number + return nil, fmt.Errorf("could not parse line: %s", line) + } + // FIXME: Trim key and value? Remove quotation marks? + branch = append(branch, sops.TreeItem{ + Key: line[:pos], + Value: line[pos+1:], + }) + } + return branch, nil +} + +func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { + plain, err := store.EmitPlainFile(in.Branch) + if err != nil { return nil, err } + buffer := bytes.NewBuffer(plain) + metadata := stores.MetadataFromInternal(in.Metadata) + str, err := ToGOB64(metadata) + if err != nil { return nil, err } + line := fmt.Sprintf("_metadata=%s\n", str) + buffer.WriteString(line) + return buffer.Bytes(), nil +} + +func (store *Store) EmitPlainFile(in sops.TreeBranch) ([]byte, error) { + buffer := bytes.Buffer{} + for _, item := range in { + // FIXME: Check that item.Value is a scalar. + // FIXME: Does Go know how to print the OS-specific EOL string? Do we care? + line := fmt.Sprintf("%s=%s\n", item.Key, item.Value) + buffer.WriteString(line) + } + return buffer.Bytes(), nil +} + +func (Store) EmitValue(v interface{}) ([]byte, error) { + // FIXME: Whot should this function do? + panic("implement me") +} + +func init() { + gob.Register(stores.Metadata{}) +} + +func ToGOB64(m stores.Metadata) (string, error) { + buf := bytes.Buffer{} + encoder := gob.NewEncoder(&buf) + err := encoder.Encode(m) + if err != nil { + return "", fmt.Errorf("could not base64-encode metadata: %s", err) + } + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} + +func FromGOB64(str string) (stores.Metadata, error) { + metadata := stores.Metadata{} + data, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return metadata, fmt.Errorf("could not base64-decode metadata: %s", err) + } + buffer := bytes.Buffer{} + buffer.Write(data) + decoder := gob.NewDecoder(&buffer) + err = decoder.Decode(&metadata) + if err != nil { + return stores.Metadata{}, fmt.Errorf("could not parse metadata: %s", err) + } + return metadata, nil +} diff --git a/stores/env/store_test.go b/stores/env/store_test.go new file mode 100644 index 000000000..198445003 --- /dev/null +++ b/stores/env/store_test.go @@ -0,0 +1,54 @@ +package env + +import ( + "github.com/stretchr/testify/assert" + "go.mozilla.org/sops" + "strings" + "testing" +) + +var PLAIN = []byte(strings.TrimLeft(` +VAR1=val1 +VAR2=val2 +VAR3_unencrypted=val3 +`, "\n")) + +var BRANCH = sops.TreeBranch{ + sops.TreeItem{ + Key: "VAR1", + Value: "val1", + }, + sops.TreeItem{ + Key: "VAR2", + Value: "val2", + }, + sops.TreeItem{ + Key: "VAR3_unencrypted", + Value: "val3", + }, +} + + +func TestLoadEncryptedFile(t *testing.T) { + // FIXME: Implementation? +} + +func TestLoadPlainFile(t *testing.T) { + branch, err := (&Store{}).LoadPlainFile(PLAIN) + assert.Nil(t, err) + assert.Equal(t, BRANCH, branch) +} + +func TestEmitEncryptedFile(t *testing.T) { + // FIXME: Implementation? +} + +func TestEmitPlainFile(t *testing.T) { + bytes, err := (&Store{}).EmitPlainFile(BRANCH) + assert.Nil(t, err) + assert.Equal(t, PLAIN, bytes) +} + +func TestEmitValue(t *testing.T) { + // FIXME: Implementation? +} diff --git a/stores/stores.go b/stores/stores.go index 19d860a4b..b45c88907 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -27,7 +27,7 @@ type SopsFile struct { // in the SOPS file by checking for nil. This way we can show the user a // helpful error message indicating that the metadata wasn't found, instead // of showing a cryptic parsing error - Metadata *Metadata `yaml:"sops" json:"sops"` + Metadata *Metadata `yaml:"sops" json:"sops" env:"sops"` } // Metadata is stored in SOPS encrypted files, and it contains the information necessary to decrypt the file. @@ -35,52 +35,52 @@ type SopsFile struct { // in order to allow the binary format to stay backwards compatible over time, but at the same time allow the internal // representation SOPS uses to change over time. type Metadata struct { - ShamirThreshold int `yaml:"shamir_threshold,omitempty" json:"shamir_threshold,omitempty"` - KeyGroups []keygroup `yaml:"key_groups,omitempty" json:"key_groups,omitempty"` - KMSKeys []kmskey `yaml:"kms" json:"kms"` - GCPKMSKeys []gcpkmskey `yaml:"gcp_kms" json:"gcp_kms"` - AzureKeyVaultKeys []azkvkey `yaml:"azure_kv" json:"azure_kv"` - LastModified string `yaml:"lastmodified" json:"lastmodified"` - MessageAuthenticationCode string `yaml:"mac" json:"mac"` - PGPKeys []pgpkey `yaml:"pgp" json:"pgp"` - UnencryptedSuffix string `yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty"` - EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty"` - Version string `yaml:"version" json:"version"` + ShamirThreshold int `yaml:"shamir_threshold,omitempty" json:"shamir_threshold,omitempty" env:"shamir_threshold,omitempty"` + KeyGroups []keygroup `yaml:"key_groups,omitempty" json:"key_groups,omitempty" env:"key_groups,omitempty"` + KMSKeys []kmskey `yaml:"kms" json:"kms" env:"kms"` + GCPKMSKeys []gcpkmskey `yaml:"gcp_kms" json:"gcp_kms" env:"gcp_kms"` + AzureKeyVaultKeys []azkvkey `yaml:"azure_kv" json:"azure_kv" env:"azure_kv"` + LastModified string `yaml:"lastmodified" json:"lastmodified" env:"lastmodified"` + MessageAuthenticationCode string `yaml:"mac" json:"mac" env:"mac"` + PGPKeys []pgpkey `yaml:"pgp" json:"pgp" env:"pgp"` + UnencryptedSuffix string `yaml:"unencrypted_suffix,omitempty" json:"unencrypted_suffix,omitempty" env:"unencrypted_suffix,omitempty"` + EncryptedSuffix string `yaml:"encrypted_suffix,omitempty" json:"encrypted_suffix,omitempty" env:"encrypted_suffix,omitempty"` + Version string `yaml:"version" json:"version" env:"version"` } type keygroup struct { - PGPKeys []pgpkey `yaml:"pgp,omitempty" json:"pgp,omitempty"` - KMSKeys []kmskey `yaml:"kms,omitempty" json:"kms,omitempty"` - GCPKMSKeys []gcpkmskey `yaml:"gcp_kms,omitempty" json:"gcp_kms,omitempty"` - AzureKeyVaultKeys []azkvkey `yaml:"azure_kv,omitempty" json:"azure_kv,omitempty"` + PGPKeys []pgpkey `yaml:"pgp,omitempty" json:"pgp,omitempty" env:"pgp,omitempty"` + KMSKeys []kmskey `yaml:"kms,omitempty" json:"kms,omitempty" env:"kms,omitempty"` + GCPKMSKeys []gcpkmskey `yaml:"gcp_kms,omitempty" json:"gcp_kms,omitempty" env:"gcp_kms,omitempty"` + AzureKeyVaultKeys []azkvkey `yaml:"azure_kv,omitempty" json:"azure_kv,omitempty" env:"azure_kv,omitempty"` } type pgpkey struct { - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` - Fingerprint string `yaml:"fp" json:"fp"` + CreatedAt string `yaml:"created_at" json:"created_at" env:"created_at"` + EncryptedDataKey string `yaml:"enc" json:"enc" env:"enc"` + Fingerprint string `yaml:"fp" json:"fp" env:"fp"` } type kmskey struct { - Arn string `yaml:"arn" json:"arn"` - Role string `yaml:"role,omitempty" json:"role,omitempty"` - Context map[string]*string `yaml:"context,omitempty" json:"context,omitempty"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + Arn string `yaml:"arn" json:"arn" env:"arn"` + Role string `yaml:"role,omitempty" json:"role,omitempty" env:"role,omitempty"` + Context map[string]*string `yaml:"context,omitempty" json:"context,omitempty" env:"context,omitempty"` + CreatedAt string `yaml:"created_at" json:"created_at" env:"created_at"` + EncryptedDataKey string `yaml:"enc" json:"enc" env:"enc"` } type gcpkmskey struct { - ResourceID string `yaml:"resource_id" json:"resource_id"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + ResourceID string `yaml:"resource_id" json:"resource_id" env:"resource_id"` + CreatedAt string `yaml:"created_at" json:"created_at" env:"created_at"` + EncryptedDataKey string `yaml:"enc" json:"enc" env:"enc"` } type azkvkey struct { - VaultURL string `yaml:"vault_url" json:"vault_url"` - Name string `yaml:"name" json:"name"` - Version string `yaml:"version" json:"version"` - CreatedAt string `yaml:"created_at" json:"created_at"` - EncryptedDataKey string `yaml:"enc" json:"enc"` + VaultURL string `yaml:"vault_url" json:"vault_url" env:"vault_url"` + Name string `yaml:"name" json:"name" env:"name"` + Version string `yaml:"version" json:"version" env:"version"` + CreatedAt string `yaml:"created_at" json:"created_at" env:"created_at"` + EncryptedDataKey string `yaml:"enc" json:"enc" env:"enc"` } // MetadataFromInternal converts an internal SOPS metadata representation to a representation appropriate for storage