2121from tests .functional .s3 .test_sync_command import TestSyncCaseConflict
2222from tests import requires_crt
2323
24+ MB = 1024 ** 2
2425
2526class BufferedBytesIO (BytesIO ):
2627 @property
@@ -33,6 +34,10 @@ class BaseCPCommandTest(BaseS3TransferCommandTest):
3334
3435
3536class TestCPCommand (BaseCPCommandTest ):
37+ def setUp (self ):
38+ super ().setUp ()
39+ self .multipart_threshold = 8 * MB
40+
3641 def test_operations_used_in_upload (self ):
3742 full_path = self .files .create_file ('foo.txt' , 'mycontent' )
3843 cmdline = '%s %s s3://bucket/key.txt' % (self .prefix , full_path )
@@ -558,6 +563,13 @@ def test_cp_with_sse_c_copy_source_fileb(self):
558563 self .assertEqual (len (self .operations_called ), 2 )
559564 self .assertEqual (self .operations_called [0 ][0 ].name , 'HeadObject' )
560565 self .assertEqual (self .operations_called [1 ][0 ].name , 'CopyObject' )
566+ expected_head_args = {
567+ 'Bucket' : 'bucket-one' ,
568+ 'Key' : 'key.txt' ,
569+ 'SSECustomerAlgorithm' : 'AES256' ,
570+ 'SSECustomerKey' : key_contents ,
571+ }
572+ self .assertDictEqual (self .operations_called [0 ][1 ], expected_head_args )
561573
562574 expected_args = {
563575 'Key' : 'key.txt' , 'Bucket' : 'bucket' ,
@@ -571,6 +583,175 @@ def test_cp_with_sse_c_copy_source_fileb(self):
571583 }
572584 self .assertDictEqual (self .operations_called [1 ][1 ], expected_args )
573585
586+ def test_s3s3_cp_with_destination_sse_c (self ):
587+ """S3->S3 copy with an unencrypted source and encrypted destination"""
588+ self .parsed_responses = [
589+ self .head_object_response (),
590+ self .copy_object_response (),
591+ ]
592+ cmdline = (
593+ '%s s3://bucket-one/key.txt s3://bucket/key.txt '
594+ '--sse-c --sse-c-key destination-key' % self .prefix
595+ )
596+ self .run_cmd (cmdline , expected_rc = 0 )
597+ self .assertEqual (len (self .operations_called ), 2 )
598+ self .assertEqual (self .operations_called [0 ][0 ].name , 'HeadObject' )
599+ expected_head_args = {
600+ 'Bucket' : 'bucket-one' ,
601+ 'Key' : 'key.txt' ,
602+ # don't expect to see SSE-c params for the source
603+ }
604+ self .assertDictEqual (self .operations_called [0 ][1 ], expected_head_args )
605+
606+ self .assertEqual (self .operations_called [1 ][0 ].name , 'CopyObject' )
607+ expected_copy_args = {
608+ 'Key' : 'key.txt' ,
609+ 'Bucket' : 'bucket' ,
610+ 'ContentType' : 'text/plain' ,
611+ 'CopySource' : {'Bucket' : 'bucket-one' , 'Key' : 'key.txt' },
612+ 'SSECustomerAlgorithm' : 'AES256' ,
613+ 'SSECustomerKey' : 'destination-key' ,
614+ }
615+ self .assertDictEqual (self .operations_called [1 ][1 ], expected_copy_args )
616+
617+ def test_s3s3_cp_with_different_sse_c_keys (self ):
618+ """S3->S3 copy with different SSE-C keys for source and destination"""
619+ self .parsed_responses = [
620+ self .head_object_response (),
621+ self .copy_object_response (),
622+ ]
623+ cmdline = (
624+ '%s s3://bucket-one/key.txt s3://bucket/key.txt '
625+ '--sse-c-copy-source --sse-c-copy-source-key foo --sse-c --sse-c-key bar'
626+ % self .prefix
627+ )
628+ self .run_cmd (cmdline , expected_rc = 0 )
629+ self .assertEqual (len (self .operations_called ), 2 )
630+ self .assertEqual (self .operations_called [0 ][0 ].name , 'HeadObject' )
631+ expected_head_args = {
632+ 'Bucket' : 'bucket-one' ,
633+ 'Key' : 'key.txt' ,
634+ 'SSECustomerAlgorithm' : 'AES256' ,
635+ 'SSECustomerKey' : 'foo' ,
636+ }
637+ self .assertDictEqual (self .operations_called [0 ][1 ], expected_head_args )
638+
639+ self .assertEqual (self .operations_called [1 ][0 ].name , 'CopyObject' )
640+ expected_copy_args = {
641+ 'Key' : 'key.txt' ,
642+ 'Bucket' : 'bucket' ,
643+ 'ContentType' : 'text/plain' ,
644+ 'CopySource' : {'Bucket' : 'bucket-one' , 'Key' : 'key.txt' },
645+ 'SSECustomerAlgorithm' : 'AES256' ,
646+ 'SSECustomerKey' : 'bar' ,
647+ 'CopySourceSSECustomerAlgorithm' : 'AES256' ,
648+ 'CopySourceSSECustomerKey' : 'foo' ,
649+ }
650+ self .assertDictEqual (self .operations_called [1 ][1 ], expected_copy_args )
651+
652+ def test_s3s3_cp_with_destination_sse_c_multipart (self ):
653+ """S3->S3 multipart copy with unencrypted source and encrypted destination"""
654+ self .parsed_responses = [
655+ self .head_object_response (ContentLength = self .multipart_threshold ),
656+ self .create_mpu_response ('upload_id' ),
657+ self .upload_part_copy_response (),
658+ self .complete_mpu_response (),
659+ ]
660+ cmdline = (
661+ '%s s3://bucket-one/key.txt s3://bucket/key.txt '
662+ '--sse-c --sse-c-key destination-key' % self .prefix
663+ )
664+ self .run_cmd (cmdline , expected_rc = 0 )
665+ self .assert_operations_called (
666+ [
667+ self .head_object_request (
668+ 'bucket-one' ,
669+ 'key.txt' ,
670+ # no SSE-C params — source is unencrypted
671+ ),
672+ self .create_mpu_request (
673+ 'bucket' ,
674+ 'key.txt' ,
675+ ContentType = 'text/plain' ,
676+ SSECustomerAlgorithm = 'AES256' ,
677+ SSECustomerKey = 'destination-key' ,
678+ ),
679+ self .upload_part_copy_request (
680+ 'bucket-one' ,
681+ 'key.txt' ,
682+ 'bucket' ,
683+ 'key.txt' ,
684+ 'upload_id' ,
685+ PartNumber = mock .ANY ,
686+ CopySourceRange = mock .ANY ,
687+ CopySourceIfMatch = '"foo-1"' ,
688+ SSECustomerAlgorithm = 'AES256' ,
689+ SSECustomerKey = 'destination-key' ,
690+ ),
691+ self .complete_mpu_request (
692+ 'bucket' ,
693+ 'key.txt' ,
694+ 'upload_id' ,
695+ num_parts = 1 ,
696+ SSECustomerAlgorithm = 'AES256' ,
697+ SSECustomerKey = 'destination-key' ,
698+ ),
699+ ]
700+ )
701+
702+ def test_s3s3_cp_with_different_sse_c_keys_multipart (self ):
703+ """S3->S3 multipart copy with different SSE-C keys"""
704+ self .parsed_responses = [
705+ self .head_object_response (ContentLength = self .multipart_threshold ),
706+ self .create_mpu_response ('upload_id' ),
707+ self .upload_part_copy_response (),
708+ self .complete_mpu_response (),
709+ ]
710+ cmdline = (
711+ '%s s3://bucket-one/key.txt s3://bucket/key.txt '
712+ '--sse-c-copy-source --sse-c-copy-source-key source-key --sse-c --sse-c-key destination-key'
713+ % self .prefix
714+ )
715+ self .run_cmd (cmdline , expected_rc = 0 )
716+ self .assert_operations_called (
717+ [
718+ self .head_object_request (
719+ 'bucket-one' ,
720+ 'key.txt' ,
721+ SSECustomerAlgorithm = 'AES256' ,
722+ SSECustomerKey = 'source-key' ,
723+ ),
724+ self .create_mpu_request (
725+ 'bucket' ,
726+ 'key.txt' ,
727+ ContentType = 'text/plain' ,
728+ SSECustomerAlgorithm = 'AES256' ,
729+ SSECustomerKey = 'destination-key' ,
730+ ),
731+ self .upload_part_copy_request (
732+ 'bucket-one' ,
733+ 'key.txt' ,
734+ 'bucket' ,
735+ 'key.txt' ,
736+ 'upload_id' ,
737+ PartNumber = mock .ANY ,
738+ CopySourceRange = mock .ANY ,
739+ CopySourceIfMatch = '"foo-1"' ,
740+ SSECustomerAlgorithm = 'AES256' ,
741+ SSECustomerKey = 'destination-key' ,
742+ CopySourceSSECustomerAlgorithm = 'AES256' ,
743+ CopySourceSSECustomerKey = 'source-key' ,
744+ ),
745+ self .complete_mpu_request (
746+ 'bucket' ,
747+ 'key.txt' ,
748+ 'upload_id' ,
749+ num_parts = 1 ,
750+ SSECustomerAlgorithm = 'AES256' ,
751+ SSECustomerKey = 'destination-key' ,
752+ ),
753+ ]
754+ )
574755
575756 # Note ideally the kms sse with a key id would be integration tests
576757 # However, you cannot delete kms keys so there would be no way to clean
0 commit comments