Skip to content

Commit b767ca3

Browse files
authored
fix(storage): enable CSEK w/ V4 signed URLs (#9450)
Closes #7626
1 parent 634f122 commit b767ca3

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

google/cloud/storage/blob.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,17 @@ def generate_signed_url(
468468
else:
469469
helper = generate_signed_url_v4
470470

471+
if self._encryption_key is not None:
472+
encryption_headers = _get_encryption_headers(self._encryption_key)
473+
if headers is None:
474+
headers = {}
475+
if version == "v2":
476+
# See: https://cloud.google.com/storage/docs/access-control/signed-urls-v2#about-canonical-extension-headers
477+
v2_copy_only = "X-Goog-Encryption-Algorithm"
478+
headers[v2_copy_only] = encryption_headers[v2_copy_only]
479+
else:
480+
headers.update(encryption_headers)
481+
471482
return helper(
472483
credentials,
473484
resource=resource,

tests/system.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import base64
1516
import datetime
17+
import hashlib
1618
import os
1719
import re
1820
import tempfile
@@ -860,11 +862,12 @@ def _create_signed_read_url_helper(
860862
version="v2",
861863
payload=None,
862864
expiration=None,
865+
encryption_key=None,
863866
):
864867
expiration = self._morph_expiration(version, expiration)
865868

866869
if payload is not None:
867-
blob = self.bucket.blob(blob_name)
870+
blob = self.bucket.blob(blob_name, encryption_key=encryption_key)
868871
blob.upload_from_string(payload)
869872
else:
870873
blob = self.blob
@@ -873,7 +876,17 @@ def _create_signed_read_url_helper(
873876
expiration=expiration, method=method, client=Config.CLIENT, version=version
874877
)
875878

876-
response = requests.get(signed_url)
879+
headers = {}
880+
881+
if encryption_key is not None:
882+
headers["x-goog-encryption-algorithm"] = "AES256"
883+
encoded_key = base64.b64encode(encryption_key).decode("utf-8")
884+
headers["x-goog-encryption-key"] = encoded_key
885+
key_hash = hashlib.sha256(encryption_key).digest()
886+
key_hash = base64.b64encode(key_hash).decode("utf-8")
887+
headers["x-goog-encryption-key-sha256"] = key_hash
888+
889+
response = requests.get(signed_url, headers=headers)
877890
self.assertEqual(response.status_code, 200)
878891
if payload is not None:
879892
self.assertEqual(response.content, payload)
@@ -916,6 +929,23 @@ def test_create_signed_read_url_v4_w_non_ascii_name(self):
916929
version="v4",
917930
)
918931

932+
def test_create_signed_read_url_v2_w_csek(self):
933+
encryption_key = os.urandom(32)
934+
self._create_signed_read_url_helper(
935+
blob_name="v2-w-csek.txt",
936+
payload=b"Test signed URL for blob w/ CSEK",
937+
encryption_key=encryption_key,
938+
)
939+
940+
def test_create_signed_read_url_v4_w_csek(self):
941+
encryption_key = os.urandom(32)
942+
self._create_signed_read_url_helper(
943+
blob_name="v2-w-csek.txt",
944+
payload=b"Test signed URL for blob w/ CSEK",
945+
encryption_key=encryption_key,
946+
version="v4",
947+
)
948+
919949
def _create_signed_delete_url_helper(self, version="v2", expiration=None):
920950
expiration = self._morph_expiration(version, expiration)
921951

tests/unit/test_blob.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,12 @@ def _generate_signed_url_helper(
391391
query_parameters=None,
392392
credentials=None,
393393
expiration=None,
394+
encryption_key=None,
394395
):
395396
from six.moves.urllib import parse
396397
from google.cloud._helpers import UTC
397398
from google.cloud.storage.blob import _API_ACCESS_ENDPOINT
399+
from google.cloud.storage.blob import _get_encryption_headers
398400

399401
api_access_endpoint = api_access_endpoint or _API_ACCESS_ENDPOINT
400402

@@ -406,7 +408,7 @@ def _generate_signed_url_helper(
406408
connection = _Connection()
407409
client = _Client(connection)
408410
bucket = _Bucket(client)
409-
blob = self._make_one(blob_name, bucket=bucket)
411+
blob = self._make_one(blob_name, bucket=bucket, encryption_key=encryption_key)
410412

411413
if version is None:
412414
effective_version = "v2"
@@ -442,6 +444,15 @@ def _generate_signed_url_helper(
442444

443445
encoded_name = blob_name.encode("utf-8")
444446
expected_resource = "/name/{}".format(parse.quote(encoded_name, safe=b"/~"))
447+
if encryption_key is not None:
448+
expected_headers = headers or {}
449+
if effective_version == "v2":
450+
expected_headers["X-Goog-Encryption-Algorithm"] = "AES256"
451+
else:
452+
expected_headers.update(_get_encryption_headers(encryption_key))
453+
else:
454+
expected_headers = headers
455+
445456
expected_kwargs = {
446457
"resource": expected_resource,
447458
"expiration": expiration,
@@ -452,7 +463,7 @@ def _generate_signed_url_helper(
452463
"response_type": response_type,
453464
"response_disposition": response_disposition,
454465
"generation": generation,
455-
"headers": headers,
466+
"headers": expected_headers,
456467
"query_parameters": query_parameters,
457468
}
458469
signer.assert_called_once_with(expected_creds, **expected_kwargs)
@@ -514,6 +525,14 @@ def test_generate_signed_url_v2_w_generation(self):
514525
def test_generate_signed_url_v2_w_headers(self):
515526
self._generate_signed_url_v2_helper(headers={"x-goog-foo": "bar"})
516527

528+
def test_generate_signed_url_v2_w_csek(self):
529+
self._generate_signed_url_v2_helper(encryption_key=os.urandom(32))
530+
531+
def test_generate_signed_url_v2_w_csek_and_headers(self):
532+
self._generate_signed_url_v2_helper(
533+
encryption_key=os.urandom(32), headers={"x-goog-foo": "bar"}
534+
)
535+
517536
def test_generate_signed_url_v2_w_credentials(self):
518537
credentials = object()
519538
self._generate_signed_url_v2_helper(credentials=credentials)
@@ -566,6 +585,14 @@ def test_generate_signed_url_v4_w_generation(self):
566585
def test_generate_signed_url_v4_w_headers(self):
567586
self._generate_signed_url_v4_helper(headers={"x-goog-foo": "bar"})
568587

588+
def test_generate_signed_url_v4_w_csek(self):
589+
self._generate_signed_url_v4_helper(encryption_key=os.urandom(32))
590+
591+
def test_generate_signed_url_v4_w_csek_and_headers(self):
592+
self._generate_signed_url_v4_helper(
593+
encryption_key=os.urandom(32), headers={"x-goog-foo": "bar"}
594+
)
595+
569596
def test_generate_signed_url_v4_w_credentials(self):
570597
credentials = object()
571598
self._generate_signed_url_v4_helper(credentials=credentials)

0 commit comments

Comments
 (0)