Skip to content

Commit

Permalink
Add customer-supplied encryption to storage.
Browse files Browse the repository at this point in the history
  • Loading branch information
daspecster committed Jun 10, 2016
1 parent b27907b commit 824bf93
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 11 deletions.
21 changes: 21 additions & 0 deletions gcloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,27 @@ def _to_bytes(value, encoding='ascii'):
raise TypeError('%r could not be converted to bytes' % (value,))


def _from_bytes(value):
"""Converts bytes to a string value, if necessary.
:type value: bytes
:param value: bytes value to attempt string conversion on.
:rtype: bytes
:returns: The original value converted to unicode (if bytes) or as passed
in if it started out as unicode.
:raises: :class:`ValueError` if the value could not be converted to
unicode.
"""
result = (value.decode('utf-8')
if isinstance(value, six.binary_type) else value)
if isinstance(result, six.text_type):
return result
else:
raise ValueError('%r could not be converted to unicode' % (value,))


def _pb_timestamp_to_datetime(timestamp):
"""Convert a Timestamp protobuf to a datetime object.
Expand Down
102 changes: 91 additions & 11 deletions gcloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

"""Create / interact with Google Cloud Storage blobs."""

import base64
import copy
import hashlib
from io import BytesIO
import json
import mimetypes
Expand All @@ -26,6 +28,8 @@
from six.moves.urllib.parse import quote

from gcloud._helpers import _rfc3339_to_datetime
from gcloud._helpers import _to_bytes
from gcloud._helpers import _from_bytes
from gcloud.credentials import generate_signed_url
from gcloud.exceptions import NotFound
from gcloud.exceptions import make_exception
Expand Down Expand Up @@ -276,17 +280,36 @@ def delete(self, client=None):
"""
return self.bucket.delete_blob(self.name, client=client)

def download_to_file(self, file_obj, client=None):
def download_to_file(self, file_obj, key=None, client=None):
"""Download the contents of this blob into a file-like object.
.. note::
If the server-set property, :attr:`media_link`, is not yet
initialized, makes an additional API request to load it.
Downloading a file that has been encrypted with a
`customer-supplied`_ key::
>>> from gcloud import storage
>>> from gcloud.storage import Blob
>>> client = storage.Client(project='my-project')
>>> bucket = client.get_bucket('my-bucket')
>>> key = 'aa426195405adee2c8081bb9e7e74b19'
>>> blob = Blob('secure-data', bucket)
>>> with open('/tmp/my-secure-file', 'wb') as file_obj:
>>> blob.download_to_file(file_obj, key=key)
.. _customer-supplied: https://cloud.google.com/storage/docs/\
encryption#customer-supplied
:type file_obj: file
:param file_obj: A file handle to which to write the blob's data.
:type key: str
:param key: Optional 32 byte key for customer-supplied encryption.
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the blob's bucket.
Expand All @@ -305,7 +328,11 @@ def download_to_file(self, file_obj, client=None):
if self.chunk_size is not None:
download.chunksize = self.chunk_size

request = Request(download_url, 'GET')
headers = {}
if key:
_set_encryption_headers(key, headers)

request = Request(download_url, 'GET', headers)

# Use the private ``_connection`` rather than the public
# ``.connection``, since the public connection may be a batch. A
Expand All @@ -315,27 +342,33 @@ def download_to_file(self, file_obj, client=None):
# it has all three (http, API_BASE_URL and build_api_url).
download.initialize_download(request, client._connection.http)

def download_to_filename(self, filename, client=None):
def download_to_filename(self, filename, key=None, client=None):
"""Download the contents of this blob into a named file.
:type filename: string
:param filename: A filename to be passed to ``open``.
:type key: str
:param key: Optional 32 byte key for customer-supplied encryption.
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the blob's bucket.
:raises: :class:`gcloud.exceptions.NotFound`
"""
with open(filename, 'wb') as file_obj:
self.download_to_file(file_obj, client=client)
self.download_to_file(file_obj, key=key, client=client)

mtime = time.mktime(self.updated.timetuple())
os.utime(file_obj.name, (mtime, mtime))

def download_as_string(self, client=None):
def download_as_string(self, key=None, client=None):
"""Download the contents of this blob as a string.
:type key: str
:param key: Optional 32 byte key for customer-supplied encryption.
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the blob's bucket.
Expand All @@ -345,7 +378,7 @@ def download_as_string(self, client=None):
:raises: :class:`gcloud.exceptions.NotFound`
"""
string_buffer = BytesIO()
self.download_to_file(string_buffer, client=client)
self.download_to_file(string_buffer, key=key, client=client)
return string_buffer.getvalue()

@staticmethod
Expand All @@ -358,7 +391,8 @@ def _check_response_error(request, http_response):
raise make_exception(faux_response, http_response.content,
error_info=request.url)

def upload_from_file(self, file_obj, rewind=False, size=None,
# pylint: disable=too-many-locals
def upload_from_file(self, file_obj, rewind=False, size=None, key=None,
content_type=None, num_retries=6, client=None):
"""Upload the contents of this blob from a file-like object.
Expand All @@ -378,6 +412,21 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
`lifecycle <https://cloud.google.com/storage/docs/lifecycle>`_
API documents for details.
Uploading a file with `customer-supplied`_ encryption::
>>> from gcloud import storage
>>> from gcloud.storage import Blob
>>> client = storage.Client(project='my-project')
>>> bucket = client.get_bucket('my-bucket')
>>> key = 'aa426195405adee2c8081bb9e7e74b19'
>>> blob = Blob('secure-data', bucket)
>>> with open('my-file', 'rb') as my_file:
>>> blob.upload_from_file(my_file, key=key)
.. _customer-supplied: https://cloud.google.com/storage/docs/\
encryption#customer-supplied
:type file_obj: file
:param file_obj: A file handle open for reading.
Expand All @@ -391,6 +440,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
:func:`os.fstat`. (If the file handle is not from the
filesystem this won't be possible.)
:type key: str
:param key: Optional 32 byte key for customer-supplied encryption.
:type content_type: string or ``NoneType``
:param content_type: Optional type of content being uploaded.
Expand Down Expand Up @@ -434,6 +486,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
'User-Agent': connection.USER_AGENT,
}

if key:
_set_encryption_headers(key, headers)

upload = Upload(file_obj, content_type, total_bytes,
auto_transfer=False)

Expand Down Expand Up @@ -473,8 +528,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
six.string_types): # pragma: NO COVER Python3
response_content = response_content.decode('utf-8')
self._set_properties(json.loads(response_content))
# pylint: enable=too-many-locals

def upload_from_filename(self, filename, content_type=None,
def upload_from_filename(self, filename, content_type=None, key=None,
client=None):
"""Upload this blob's contents from the content of a named file.
Expand All @@ -500,6 +556,9 @@ def upload_from_filename(self, filename, content_type=None,
:type content_type: string or ``NoneType``
:param content_type: Optional type of content being uploaded.
:type key: str
:param key: Optional 32 byte key for customer-supplied encryption.
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the blob's bucket.
Expand All @@ -509,10 +568,10 @@ def upload_from_filename(self, filename, content_type=None,
content_type, _ = mimetypes.guess_type(filename)

with open(filename, 'rb') as file_obj:
self.upload_from_file(file_obj, content_type=content_type,
self.upload_from_file(file_obj, content_type=content_type, key=key,
client=client)

def upload_from_string(self, data, content_type='text/plain',
def upload_from_string(self, data, content_type='text/plain', key=None,
client=None):
"""Upload contents of this blob from the provided string.
Expand All @@ -535,6 +594,9 @@ def upload_from_string(self, data, content_type='text/plain',
:param content_type: Optional type of content being uploaded. Defaults
to ``'text/plain'``.
:type key: str
:param key: Optional 32 byte key for customer-supplied encryption.
:type client: :class:`gcloud.storage.client.Client` or ``NoneType``
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the blob's bucket.
Expand All @@ -545,7 +607,7 @@ def upload_from_string(self, data, content_type='text/plain',
string_buffer.write(data)
self.upload_from_file(file_obj=string_buffer, rewind=True,
size=len(data), content_type=content_type,
client=client)
key=key, client=client)

def make_public(self, client=None):
"""Make this blob public giving all users read access.
Expand Down Expand Up @@ -838,3 +900,21 @@ def __init__(self, bucket_name, object_name):
self.query_params = {'name': object_name}
self._bucket_name = bucket_name
self._relative_path = ''


def _set_encryption_headers(key, headers):
"""Builds customer encyrption key headers
:type key: str
:param key: 32 byte key to build request key and hash.
:type headers: dict
:param headers: dict of HTTP headers being sent in request.
"""
key = _to_bytes(key)
sha256_key = hashlib.sha256(key).digest()
key_hash = base64.b64encode(sha256_key).rstrip()
encoded_key = base64.b64encode(key).rstrip()
headers['X-Goog-Encryption-Algorithm'] = 'AES256'
headers['X-Goog-Encryption-Key'] = _from_bytes(encoded_key)
headers['X-Goog-Encryption-Key-Sha256'] = _from_bytes(key_hash)
Loading

0 comments on commit 824bf93

Please sign in to comment.