diff --git a/decrypt/decrypt.go b/decrypt/decrypt.go new file mode 100644 index 000000000..ea2a944c8 --- /dev/null +++ b/decrypt/decrypt.go @@ -0,0 +1,79 @@ +package decrypt // import "go.mozilla.org/sops/decrypt" + +import ( + "fmt" + "io/ioutil" + "time" + + "go.mozilla.org/sops" + "go.mozilla.org/sops/aes" + sopsjson "go.mozilla.org/sops/json" + sopsyaml "go.mozilla.org/sops/yaml" +) + +// 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 := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("Failed to read %q: %v", path, err) + } + return Data(encryptedData, format) +} + +// 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` or `binary`. +// If the format string is empty, binary format is assumed. +func Data(data []byte, format string) (cleartext []byte, err error) { + // Initialize a Sops JSON store + var store sops.Store + switch format { + case "json": + store = &sopsjson.Store{} + case "yaml": + store = &sopsyaml.Store{} + default: + store = &sopsjson.BinaryStore{} + } + // Load Sops metadata from the document and access the data key + metadata, err := store.UnmarshalMetadata(data) + if err != nil { + return nil, fmt.Errorf("Failed to unmarshal sops metadata: %v", err) + } + key, err := metadata.GetDataKey() + if err != nil { + return nil, fmt.Errorf("Failed to get data key: %+v", err) + } + + // Load the encrypted document and create a tree structure + // with the encrypted content and metadata + branch, err := store.Unmarshal(data) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal sops encrypted branch: %v", err) + } + tree := sops.Tree{Branch: branch, Metadata: metadata} + + // Decrypt the tree + cipher := aes.Cipher{} + stash := make(map[string][]interface{}) + mac, err := tree.Decrypt(key, cipher, stash) + if err != nil { + return nil, fmt.Errorf("Failed to decrypt tree: %v", 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( + metadata.MessageAuthenticationCode, + key, + metadata.LastModified.Format(time.RFC3339), + ) + if originalMac != mac { + return nil, fmt.Errorf("Failed to verify data integrity. expected mac %q, got %q", originalMac, mac) + } + + return store.Marshal(tree.Branch) +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 000000000..318260077 --- /dev/null +++ b/example_test.go @@ -0,0 +1,48 @@ +package sops_test + +import ( + "encoding/json" + "log" + + "go.mozilla.org/sops/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 Example_DecryptFile() { + var ( + confPath string = "./example.json" + cfg configuration + err error + ) + confData, err := decrypt.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) +}