Skip to content

Commit f41d74f

Browse files
jhellerDmitriy Rabotyagov
authored andcommitted
Add support for SSE-C encryption
Changes implement 2 new flags --sse-customer-key and --sse-copy-source-customer-key that can be used by user to provide a key for server side encryption. Once these options are set extra headers are added to request accordingly to SSE-C specification [1] This PR squashes and rebases on current master changes implemented by @jheller [1] https://docs.aws.amazon.com/AmazonS3/latest/userguide/specifying-s3-c-encryption.html
1 parent d705dcd commit f41d74f

File tree

6 files changed

+101
-13
lines changed

6 files changed

+101
-13
lines changed

S3/Config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class Config(object):
132132
extra_headers = SortedDict(ignore_case = True)
133133
force = False
134134
server_side_encryption = False
135+
sse_customer_key = ""
135136
enable = None
136137
get_continue = False
137138
put_continue = False

S3/FileDict.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def get_md5(self, relative_file):
4343
if 'md5' in self[relative_file]:
4444
return self[relative_file]['md5']
4545
md5 = self.get_hardlink_md5(relative_file)
46-
if md5 is None and 'md5' in cfg.sync_checks:
46+
if md5 is None and 'md5' in cfg.preserve_attrs_list:
4747
logging.debug(u"doing file I/O to read md5 of %s" % relative_file)
4848
md5 = Utils.hash_file_md5(self[relative_file]['full_name'])
4949
self.record_md5(relative_file, md5)

S3/FileLists.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,23 @@ def _compare(src_list, dst_lst, src_remote, dst_remote, file):
552552
attribs_match = False
553553
debug(u"XFER: %s (md5 mismatch: src=%s dst=%s)" % (file, src_md5, dst_md5))
554554

555+
# Check mtime. This compares local mtime to the upload time of remote file
556+
compare_mtime = 'mtime' in cfg.sync_checks
557+
if attribs_match and compare_mtime:
558+
try:
559+
src_mtime = src_list[file]['mtime']
560+
dst_mtime = dst_list[file]['timestamp']
561+
except (IOError,OSError):
562+
# mtime sum verification failed - ignore that file altogether
563+
debug(u"IGNR: %s (disappeared)" % (file))
564+
warning(u"%s: file disappeared, ignoring." % (file))
565+
raise
566+
567+
if src_mtime > dst_mtime:
568+
## checksums are different.
569+
attribs_match = False
570+
debug(u"XFER: %s (mtime newer than last upload: src=%s dst=%s)" % (file, src_mtime, dst_mtime))
571+
555572
return attribs_match
556573

557574
# we don't support local->local sync, use 'rsync' or something like that instead ;-)

S3/S3.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,19 +1828,32 @@ def send_file(self, request, stream, labels, buffer = '', throttle = 0,
18281828
## Non-recoverable error
18291829
raise S3Error(response)
18301830

1831-
debug("MD5 sums: computed=%s, received=%s" % (md5_computed, response["headers"].get('etag', '').strip('"\'')))
1832-
## when using KMS encryption, MD5 etag value will not match
1833-
md5_from_s3 = response["headers"].get("etag", "").strip('"\'')
1834-
if ('-' not in md5_from_s3) and (md5_from_s3 != md5_hash.hexdigest()) and response["headers"].get("x-amz-server-side-encryption") != 'aws:kms':
1835-
warning("MD5 Sums don't match!")
1836-
if retries:
1837-
warning("Retrying upload of %s" % (filename))
1838-
return self.send_file(request, stream, labels, buffer, throttle,
1839-
retries - 1, offset, chunk_size, use_expect_continue)
1831+
if self.config.sse_customer_key:
1832+
if response["headers"]["x-amz-server-side-encryption-customer-key-md5"] != \
1833+
self.config.extra_headers["x-amz-server-side-encryption-customer-key-md5"]:
1834+
warning("MD5 of customer key don't match!")
1835+
if retries:
1836+
warning("Retrying upload of %s" % (filename))
1837+
return self.send_file(request, stream, labels, buffer, throttle, retries - 1, offset, chunk_size)
1838+
else:
1839+
warning("Too many failures. Giving up on '%s'" % (filename))
1840+
raise S3UploadError
18401841
else:
1841-
warning("Too many failures. Giving up on '%s'" % (filename))
1842-
raise S3UploadError("Too many failures. Giving up on '%s'"
1843-
% filename)
1842+
debug("Match of x-amz-server-side-encryption-customer-key-md5")
1843+
else:
1844+
debug("MD5 sums: computed=%s, received=%s" % (md5_computed, response["headers"].get('etag', '').strip('"\'')))
1845+
## when using KMS encryption, MD5 etag value will not match
1846+
md5_from_s3 = response["headers"].get("etag", "").strip('"\'')
1847+
if ('-' not in md5_from_s3) and (md5_from_s3 != md5_hash.hexdigest()) and response["headers"].get("x-amz-server-side-encryption") != 'aws:kms':
1848+
warning("MD5 Sums don't match!")
1849+
if retries:
1850+
warning("Retrying upload of %s" % (filename))
1851+
return self.send_file(request, stream, labels, buffer, throttle,
1852+
retries - 1, offset, chunk_size, use_expect_continue)
1853+
else:
1854+
warning("Too many failures. Giving up on '%s'" % (filename))
1855+
raise S3UploadError("Too many failures. Giving up on '%s'"
1856+
% filename)
18441857

18451858
return response
18461859

s3cmd

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ if sys.version_info < (2, 6):
3030

3131
PY3 = (sys.version_info >= (3, 0))
3232

33+
import base64
3334
import codecs
3435
import errno
3536
import glob
37+
import hashlib
3638
import io
3739
import locale
3840
import logging
@@ -1920,6 +1922,15 @@ def cmd_sync_local2remote(args):
19201922
error(u"or disable encryption with --no-encrypt parameter.")
19211923
sys.exit(EX_USAGE)
19221924

1925+
# Disable md5 checks if using SSE-C. Add mtime check
1926+
if cfg.sse_customer_key:
1927+
try:
1928+
cfg.sync_checks.remove("md5")
1929+
except Exception:
1930+
pass
1931+
if cfg.sync_checks.count("mtime") == 0:
1932+
cfg.sync_checks.append("mtime")
1933+
19231934
for arg in args[:-1]:
19241935
if not os.path.exists(deunicodise(arg)):
19251936
raise ParameterError("Invalid source: '%s' is not an existing file or directory" % arg)
@@ -2365,6 +2376,7 @@ def run_configure(config_file, args):
23652376
("gpg_passphrase", "Encryption password", "Encryption password is used to protect your files from reading\nby unauthorized persons while in transfer to S3"),
23662377
("gpg_command", "Path to GPG program"),
23672378
("use_https", "Use HTTPS protocol", "When using secure HTTPS protocol all communication with Amazon S3\nservers is protected from 3rd party eavesdropping. This method is\nslower than plain HTTP, and can only be proxied with Python 2.7 or newer"),
2379+
("sse_customer_key", "Encryption key for server-side-encryption with customer key.\nMust be 32 characters"),
23682380
("proxy_host", "HTTP Proxy server name", "On some networks all internet access must go through a HTTP proxy.\nTry setting it here if you can't connect to S3 directly"),
23692381
("proxy_port", "HTTP Proxy server port"),
23702382
]
@@ -2804,6 +2816,8 @@ def main():
28042816

28052817
optparser.add_option( "--server-side-encryption", dest="server_side_encryption", action="store_true", help="Specifies that server-side encryption will be used when putting objects. [put, sync, cp, modify]")
28062818
optparser.add_option( "--server-side-encryption-kms-id", dest="kms_key", action="store", help="Specifies the key id used for server-side encryption with AWS KMS-Managed Keys (SSE-KMS) when putting objects. [put, sync, cp, modify]")
2819+
optparser.add_option( "--sse-customer-key", dest="sse_customer_key", action="store", metavar="12345678901234567890123456789012", help="Specifies a customer provided key for server-side encryption. Must be 32 character string.")
2820+
optparser.add_option( "--sse-copy-source-customer-key", dest="sse_copy_source_customer_key", action="store", metavar="12345678901234567890123456789012", help="Specifies the encryption key for copying or moving objects with a customer provided key for server-side encryption.")
28072821

28082822
optparser.add_option( "--encoding", dest="encoding", metavar="ENCODING", help="Override autodetected terminal and filesystem encoding (character set). Autodetected: %s" % autodetected_encoding)
28092823
optparser.add_option( "--add-encoding-exts", dest="add_encoding_exts", metavar="EXTENSIONs", help="Add encoding to these comma delimited extensions i.e. (css,js,html) when uploading to S3 )")
@@ -2917,6 +2931,41 @@ def main():
29172931
error(u"Option --progress is not yet supported on MS Windows platform. Assuming --no-progress.")
29182932
cfg.progress_meter = False
29192933

2934+
if options.sse_customer_key is not None:
2935+
if len(options.sse_customer_key) == 32:
2936+
cfg.sse_customer_key = options.sse_customer_key
2937+
else:
2938+
error(u"sse-customer-key must be 32 characters")
2939+
sys.exit(EX_CONFIG)
2940+
2941+
if cfg.sse_customer_key:
2942+
md5 = hashlib.md5()
2943+
sse_customer_key = cfg.sse_customer_key.encode()
2944+
md5.update(sse_customer_key)
2945+
md5_encoded = base64.b64encode(md5.digest())
2946+
encoded = base64.b64encode(sse_customer_key)
2947+
cfg.extra_headers["x-amz-server-side-encryption-customer-algorithm"] = "AES256"
2948+
cfg.extra_headers["x-amz-server-side-encryption-customer-key"] = encoded.decode()
2949+
cfg.extra_headers["x-amz-server-side-encryption-customer-key-md5"] = md5_encoded.decode()
2950+
2951+
debug(u"Updating Config.Config extra_headers[%s] -> %s" % ("x-amz-server-side-encryption-customer-algorithm", "AES256"))
2952+
debug(u"Updating Config.Config extra_headers[%s] -> %s" % ("x-amz-server-side-encryption-customer-key", encoded))
2953+
debug(u"Updating Config.Config extra_headers[%s] -> %s" % ("x-amz-server-side-encryption-customer-key-md5", md5_encoded))
2954+
2955+
if options.sse_copy_source_customer_key is not None:
2956+
md5 = hashlib.md5()
2957+
sse_copy_source_customer_key = options.sse_copy_source_customer_key.encode()
2958+
md5.update(sse_copy_source_customer_key)
2959+
md5_encoded = base64.b64encode(md5.digest())
2960+
encoded = base64.b64encode(sse_copy_source_customer_key)
2961+
cfg.extra_headers["x-amz-copy-source-server-side-encryption-customer-algorithm"] = "AES256"
2962+
cfg.extra_headers["x-amz-copy-source-server-side-encryption-customer-key"] = encoded.decode()
2963+
cfg.extra_headers["x-amz-copy-source-server-side-encryption-customer-key-md5"] = md5_encoded.decode()
2964+
2965+
debug(u"Updating Config.Config extra_headers[%s] -> %s" % ("x-amz-copy-source-server-side-encryption-customer-algorithm", "AES256"))
2966+
debug(u"Updating Config.Config extra_headers[%s] -> %s" % ("x-amz-copy-source-server-side-encryption-customer-key", encoded))
2967+
debug(u"Updating Config.Config extra_headers[%s] -> %s" % ("x-amz-copy-source-server-side-encryption-customer-key-md5", md5_encoded))
2968+
29202969
## Pre-process --add-header's and put them to Config.extra_headers SortedDict()
29212970
if options.add_header:
29222971
for hdr in options.add_header:

s3cmd.1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,14 @@ Specifies the key id used for server\-side encryption
417417
with AWS KMS\-Managed Keys (SSE\-KMS) when putting
418418
objects. [put, sync, cp, modify]
419419
.TP
420+
\fB\-\-sse\-customer\-key\fR=12345678901234567890123456789012
421+
Specifies a customer key for server-side encryption, to be used
422+
when putting objects. Must be 32 characters.
423+
.TP
424+
\fB\-\-sse\-copy\-source\-customer\-key\fR=12345678901234567890123456789012
425+
Specifies the key for copying objects with server-side
426+
encryption customer key. Must be 32 characters.
427+
.TP
420428
\fB\-\-encoding\fR=ENCODING
421429
Override autodetected terminal and filesystem encoding
422430
(character set). Autodetected: UTF\-8

0 commit comments

Comments
 (0)