diff --git a/.changelog/4992.txt b/.changelog/4992.txt new file mode 100644 index 00000000000..a43fffa76c2 --- /dev/null +++ b/.changelog/4992.txt @@ -0,0 +1,4 @@ +```release-note:enhancement +added support for Customer Supplied Encryption Key (CSEK) + +``` diff --git a/google/resource_storage_bucket_object.go b/google/resource_storage_bucket_object.go index e9870da0247..7f4acbe775a 100644 --- a/google/resource_storage_bucket_object.go +++ b/google/resource_storage_bucket_object.go @@ -11,8 +11,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "crypto/md5" + "crypto/sha256" "encoding/base64" "io/ioutil" + "net/http" "google.golang.org/api/googleapi" "google.golang.org/api/storage/v1" @@ -158,19 +160,57 @@ func resourceStorageBucketObject() *schema.Resource { Optional: true, ForceNew: true, Computed: true, + ConflictsWith: []string{"customer_encryption"}, DiffSuppressFunc: compareCryptoKeyVersions, Description: `Resource name of the Cloud KMS key that will be used to encrypt the object. Overrides the object metadata's kmsKeyName value, if any.`, }, + + "customer_encryption": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"kms_key_name"}, + Description: `Encryption key; encoded using base64.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "encryption_algorithm": { + Type: schema.TypeString, + Optional: true, + Default: "AES256", + ForceNew: true, + Description: `The encryption algorithm. Default: AES256`, + }, + "encryption_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Sensitive: true, + Description: `Base64 encoded customer supplied encryption key.`, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + _, err := base64.StdEncoding.DecodeString(val.(string)) + if err != nil { + errs = append(errs, fmt.Errorf("Failed to decode (base64) customer_encryption, expecting valid base64 encoded key")) + } + return + }, + }, + }, + }, + }, + "event_based_hold": { Type: schema.TypeBool, Optional: true, Description: `Whether an object is under event-based hold. Event-based hold is a way to retain objects until an event occurs, which is signified by the hold's release (i.e. this value is set to false). After being released (set to false), such objects will be subject to bucket-level retention (if any).`, }, + "temporary_hold": { Type: schema.TypeBool, Optional: true, Description: `Whether an object is under temporary hold. While this flag is set to true, the object is protected against deletion and overwrites.`, }, + "metadata": { Type: schema.TypeMap, Optional: true, @@ -288,6 +328,12 @@ func resourceStorageBucketObjectCreate(d *schema.ResourceData, meta interface{}) insertCall.Name(name) insertCall.Media(media) + // This is done late as we need to add headers to enable customer encryption + if v, ok := d.GetOk("customer_encryption"); ok { + customerEncryption := expandCustomerEncryption(v.([]interface{})) + setEncryptionHeaders(customerEncryption, insertCall.Header()) + } + _, err = insertCall.Do() if err != nil { @@ -348,6 +394,11 @@ func resourceStorageBucketObjectRead(d *schema.ResourceData, meta interface{}) e objectsService := storage.NewObjectsService(config.NewStorageClient(userAgent)) getCall := objectsService.Get(bucket, name) + if v, ok := d.GetOk("customer_encryption"); ok { + customerEncryption := expandCustomerEncryption(v.([]interface{})) + setEncryptionHeaders(customerEncryption, getCall.Header()) + } + res, err := getCall.Do() if err != nil { @@ -438,13 +489,20 @@ func resourceStorageBucketObjectDelete(d *schema.ResourceData, meta interface{}) return nil } +func setEncryptionHeaders(customerEncryption map[string]string, headers http.Header) { + decodedKey, _ := base64.StdEncoding.DecodeString(customerEncryption["encryption_key"]) + keyHash := sha256.Sum256(decodedKey) + headers.Set("x-goog-encryption-algorithm", customerEncryption["encryption_algorithm"]) + headers.Set("x-goog-encryption-key", customerEncryption["encryption_key"]) + headers.Set("x-goog-encryption-key-sha256", base64.StdEncoding.EncodeToString(keyHash[:])) +} + func getFileMd5Hash(filename string) string { data, err := ioutil.ReadFile(filename) if err != nil { log.Printf("[WARN] Failed to read source file %q. Cannot compute md5 hash for it.", filename) return "" } - return getContentMd5Hash(data) } @@ -455,3 +513,16 @@ func getContentMd5Hash(content []byte) string { } return base64.StdEncoding.EncodeToString(h.Sum(nil)) } + +func expandCustomerEncryption(input []interface{}) map[string]string { + expanded := make(map[string]string) + if input == nil { + return expanded + } + for _, v := range input { + original := v.(map[string]interface{}) + expanded["encryption_key"] = original["encryption_key"].(string) + expanded["encryption_algorithm"] = original["encryption_algorithm"].(string) + } + return expanded +} diff --git a/google/resource_storage_bucket_object_test.go b/google/resource_storage_bucket_object_test.go index 5e522ba91a9..f63cc11704c 100644 --- a/google/resource_storage_bucket_object_test.go +++ b/google/resource_storage_bucket_object_test.go @@ -2,6 +2,7 @@ package google import ( "crypto/md5" + "crypto/sha256" "encoding/base64" "fmt" "io/ioutil" @@ -315,6 +316,39 @@ func TestAccStorageObjectKms(t *testing.T) { }) } +func TestAccStorageObject_customerEncryption(t *testing.T) { + t.Parallel() + + bucketName := testBucketName(t) + data := []byte(content) + h := md5.New() + if _, err := h.Write(data); err != nil { + t.Errorf("error calculating md5: %v", err) + } + dataMd5 := base64.StdEncoding.EncodeToString(h.Sum(nil)) + testFile := getNewTmpTestFile(t, "tf-test") + if err := ioutil.WriteFile(testFile.Name(), data, 0644); err != nil { + t.Errorf("error writing file: %v", err) + } + + customerEncryptionKey := "qI6+xvCZE9jUm94nJWIulFc8rthN64ybkGCsLUY9Do4=" + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccStorageObjectDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleStorageBucketsObjectCustomerEncryption(bucketName, customerEncryptionKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleStorageObjectWithEncryption(t, bucketName, objectName, dataMd5, customerEncryptionKey), + resource.TestCheckResourceAttr( + "google_storage_bucket_object.object", "customer_encryption.0.encryption_key", customerEncryptionKey), + ), + }, + }, + }) +} + func TestAccStorageObject_holds(t *testing.T) { t.Parallel() @@ -360,12 +394,24 @@ func TestAccStorageObject_holds(t *testing.T) { } func testAccCheckGoogleStorageObject(t *testing.T, bucket, object, md5 string) resource.TestCheckFunc { + return testAccCheckGoogleStorageObjectWithEncryption(t, bucket, object, md5, "") +} + +func testAccCheckGoogleStorageObjectWithEncryption(t *testing.T, bucket, object, md5 string, customerEncryptionKey string) resource.TestCheckFunc { return func(s *terraform.State) error { config := googleProviderConfig(t) objectsService := storage.NewObjectsService(config.NewStorageClient(config.userAgent)) getCall := objectsService.Get(bucket, object) + if customerEncryptionKey != "" { + decodedKey, _ := base64.StdEncoding.DecodeString(customerEncryptionKey) + keyHash := sha256.Sum256(decodedKey) + headers := getCall.Header() + headers.Set("x-goog-encryption-algorithm", "AES256") + headers.Set("x-goog-encryption-key", customerEncryptionKey) + headers.Set("x-goog-encryption-key-sha256", base64.StdEncoding.EncodeToString(keyHash[:])) + } res, err := getCall.Do() if err != nil { @@ -516,6 +562,23 @@ resource "google_storage_bucket_object" "object" { `, bucketName, objectName, content) } +func testGoogleStorageBucketsObjectCustomerEncryption(bucketName string, customerEncryptionKey string) string { + return fmt.Sprintf(` +resource "google_storage_bucket" "bucket" { + name = "%s" +} + +resource "google_storage_bucket_object" "object" { + name = "%s" + bucket = google_storage_bucket.bucket.name + content = "%s" + customer_encryption { + encryption_key = "%s" + } +} +`, bucketName, objectName, content, customerEncryptionKey) +} + func testGoogleStorageBucketsObjectHolds(bucketName string, eventBasedHold bool, temporaryHold bool) string { return fmt.Sprintf(` resource "google_storage_bucket" "bucket" { diff --git a/website/docs/r/storage_bucket_object.html.markdown b/website/docs/r/storage_bucket_object.html.markdown index b6412b17bcd..d7da59fb3df 100644 --- a/website/docs/r/storage_bucket_object.html.markdown +++ b/website/docs/r/storage_bucket_object.html.markdown @@ -59,12 +59,23 @@ One of the following is required: * `content_type` - (Optional) [Content-Type](https://tools.ietf.org/html/rfc7231#section-3.1.1.5) of the object data. Defaults to "application/octet-stream" or "text/plain; charset=utf-8". +* `customer_encryption` - (Optional) Enables object encryption with Customer-Supplied Encryption Key (CSEK). [Google documentation about CSEK.](https://cloud.google.com/storage/docs/encryption/customer-supplied-keys) + Structure is documented below. + * `storage_class` - (Optional) The [StorageClass](https://cloud.google.com/storage/docs/storage-classes) of the new bucket object. Supported values include: `MULTI_REGIONAL`, `REGIONAL`, `NEARLINE`, `COLDLINE`, `ARCHIVE`. If not provided, this defaults to the bucket's default storage class or to a [standard](https://cloud.google.com/storage/docs/storage-classes#standard) class. * `kms_key_name` - (Optional) The resource name of the Cloud KMS key that will be used to [encrypt](https://cloud.google.com/storage/docs/encryption/using-customer-managed-keys) the object. +--- + +The `customer_encryption` block supports: + +* `encryption_algorithm` - (Optional) Encryption algorithm. Default: AES256 + +* `encryption_key` - (Required) Base64 encoded Customer-Supplied Encryption Key. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are