Repository: googleapis/google-cloud-python

Issue:
Storage: 'Blob.reload' does not pass encryption headers
Per the [docs on customer-supplied encryption keys](https://cloud.google.com/storage/docs/encryption/customer-supplied-keys#encrypted-elements), the `X-Goog-Encryption-Key` header applies not only the the blob / object's payload, but also to its CRC32c  checksum and MD5 hash fields.

`Blob.reload` (inherited from `_helpers._PropertyMixin` does not pass those headers, and thus the returned resource will not have the `crc32c` or `md5Hash` fields set.


Discussion:

Resolving patch:
diff --git a/storage/google/cloud/storage/_helpers.py b/storage/google/cloud/storage/_helpers.py
--- a/storage/google/cloud/storage/_helpers.py
+++ b/storage/google/cloud/storage/_helpers.py
@@ -86,6 +86,18 @@ def _require_client(self, client):
             client = self.client
         return client
 
+    def _encryption_headers(self):
+        """Return any encryption headers needed to fetch the object.
+
+        .. note::
+           Defined here because :meth:`reload` calls it, but this method is
+           really only relevant for :class:`~google.cloud.storage.blob.Blob`.
+
+        :rtype: dict
+        :returns: a mapping of encryption-related headers.
+        """
+        return {}
+
     def reload(self, client=None):
         """Reload properties from Cloud Storage.
 
@@ -103,7 +115,11 @@ def reload(self, client=None):
         if self.user_project is not None:
             query_params["userProject"] = self.user_project
         api_response = client._connection.api_request(
-            method="GET", path=self.path, query_params=query_params, _target_object=self
+            method="GET",
+            path=self.path,
+            query_params=query_params,
+            headers=self._encryption_headers(),
+            _target_object=self
         )
         self._set_properties(api_response)
 
diff --git a/storage/google/cloud/storage/blob.py b/storage/google/cloud/storage/blob.py
--- a/storage/google/cloud/storage/blob.py
+++ b/storage/google/cloud/storage/blob.py
@@ -257,6 +257,14 @@ def user_project(self):
         """
         return self.bucket.user_project
 
+    def _encryption_headers(self):
+        """Return any encryption headers needed to fetch the object.
+
+        :rtype: List(Tuple(str, str))
+        :returns: a list of tuples to be passed as headers.
+        """
+        return _get_encryption_headers(self._encryption_key)
+
     @property
     def public_url(self):
         """The public URL for this blob.
diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py
--- a/storage/google/cloud/storage/bucket.py
+++ b/storage/google/cloud/storage/bucket.py
@@ -36,7 +36,6 @@
 from google.cloud.storage.acl import BucketACL
 from google.cloud.storage.acl import DefaultObjectACL
 from google.cloud.storage.blob import Blob
-from google.cloud.storage.blob import _get_encryption_headers
 from google.cloud.storage.notification import BucketNotification
 from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT
 
@@ -671,31 +670,18 @@ def get_blob(self, blob_name, client=None, encryption_key=None, **kwargs):
         :rtype: :class:`google.cloud.storage.blob.Blob` or None
         :returns: The blob object if it exists, otherwise None.
         """
-        client = self._require_client(client)
-        query_params = {}
-
-        if self.user_project is not None:
-            query_params["userProject"] = self.user_project
         blob = Blob(
             bucket=self, name=blob_name, encryption_key=encryption_key, **kwargs
         )
         try:
-            headers = _get_encryption_headers(encryption_key)
-            response = client._connection.api_request(
-                method="GET",
-                path=blob.path,
-                query_params=query_params,
-                headers=headers,
-                _target_object=blob,
-            )
-            # NOTE: We assume response.get('name') matches `blob_name`.
-            blob._set_properties(response)
             # NOTE: This will not fail immediately in a batch. However, when
             #       Batch.finish() is called, the resulting `NotFound` will be
             #       raised.
-            return blob
+            blob.reload(client=client)
         except NotFound:
             return None
+        else:
+            return blob
 
     def list_blobs(
         self,


Test patch:
diff --git a/storage/tests/unit/test__helpers.py b/storage/tests/unit/test__helpers.py
--- a/storage/tests/unit/test__helpers.py
+++ b/storage/tests/unit/test__helpers.py
@@ -55,6 +55,10 @@ def test_user_project_is_abstract(self):
         with self.assertRaises(NotImplementedError):
             mixin.user_project
 
+    def test__encryption_headers(self):
+        mixin = self._make_one()
+        self.assertEqual(mixin._encryption_headers(), {})
+
     def test_reload(self):
         connection = _Connection({"foo": "Foo"})
         client = _Client(connection)
@@ -72,6 +76,7 @@ def test_reload(self):
                 "method": "GET",
                 "path": "/path",
                 "query_params": {"projection": "noAcl"},
+                "headers": {},
                 "_target_object": derived,
             },
         )
@@ -95,6 +100,7 @@ def test_reload_w_user_project(self):
                 "method": "GET",
                 "path": "/path",
                 "query_params": {"projection": "noAcl", "userProject": user_project},
+                "headers": {},
                 "_target_object": derived,
             },
         )
diff --git a/storage/tests/unit/test_blob.py b/storage/tests/unit/test_blob.py
--- a/storage/tests/unit/test_blob.py
+++ b/storage/tests/unit/test_blob.py
@@ -280,6 +280,27 @@ def test_user_project(self):
         blob = self._make_one(blob_name, bucket=bucket)
         self.assertEqual(blob.user_project, user_project)
 
+    def test__encryption_headers_wo_encryption_key(self):
+        BLOB_NAME = "blob-name"
+        bucket = _Bucket()
+        blob = self._make_one(BLOB_NAME, bucket=bucket)
+        expected = {}
+        self.assertEqual(blob._encryption_headers(), expected)
+
+    def test__encryption_headers_w_encryption_key(self):
+        key = b"aa426195405adee2c8081bb9e7e74b19"
+        header_key_value = "YWE0MjYxOTU0MDVhZGVlMmM4MDgxYmI5ZTdlNzRiMTk="
+        header_key_hash_value = "V3Kwe46nKc3xLv96+iJ707YfZfFvlObta8TQcx2gpm0="
+        BLOB_NAME = "blob-name"
+        bucket = _Bucket()
+        blob = self._make_one(BLOB_NAME, bucket=bucket, encryption_key=key)
+        expected = {
+            "X-Goog-Encryption-Algorithm": "AES256",
+            "X-Goog-Encryption-Key": header_key_value,
+            "X-Goog-Encryption-Key-Sha256": header_key_hash_value,
+        }
+        self.assertEqual(blob._encryption_headers(), expected)
+
     def test_public_url(self):
         BLOB_NAME = "blob-name"
         bucket = _Bucket()
diff --git a/storage/tests/unit/test_bucket.py b/storage/tests/unit/test_bucket.py
--- a/storage/tests/unit/test_bucket.py
+++ b/storage/tests/unit/test_bucket.py
@@ -669,9 +669,13 @@ def test_get_blob_hit_w_user_project(self):
         self.assertIs(blob.bucket, bucket)
         self.assertEqual(blob.name, BLOB_NAME)
         kw, = connection._requested
+        expected_qp = {
+            "userProject": USER_PROJECT,
+            "projection": "noAcl",
+        }
         self.assertEqual(kw["method"], "GET")
         self.assertEqual(kw["path"], "/b/%s/o/%s" % (NAME, BLOB_NAME))
-        self.assertEqual(kw["query_params"], {"userProject": USER_PROJECT})
+        self.assertEqual(kw["query_params"], expected_qp)
 
     def test_get_blob_hit_with_kwargs(self):
         from google.cloud.storage.blob import _get_encryption_headers