<?php namespace Aws\Test\S3\Crypto; use Aws\CommandInterface; use Aws\Middleware; use Aws\S3\Crypto\S3EncryptionMultipartUploaderV2; use Aws\Result; use Aws\Crypto\KmsMaterialsProviderV2; use Aws\S3\Crypto\InstructionFileMetadataStrategy; use Aws\Test\Crypto\UsesCryptoParamsTraitV2; use Aws\Test\UsesServiceTrait; use Aws\Test\Crypto\UsesMetadataEnvelopeTrait; use GuzzleHttp\Psr7; use Yoast\PHPUnitPolyfills\TestCases\TestCase; use Psr\Http\Message\RequestInterface; class S3EncryptionMultipartUploaderV2Test extends TestCase { use UsesServiceTrait, UsesMetadataEnvelopeTrait, UsesCryptoParamsTraitV2; const MB = 1048576; const TEST_URL = 'http://foo.s3.amazonaws.com/bar'; protected function getS3Client() { static $client = null; if (!$client) { $client = $this->getTestClient('S3'); } return $client; } protected function getKmsClient() { static $client = null; if (!$client) { $client = $this->getTestClient('Kms'); } return $client; } private function setupProvidedExpectedException($exception) { if (method_exists($this, 'expectException')) { $this->expectException($exception[0]); $this->expectExceptionMessage($exception[1]); } else { $this->setExpectedException($exception[0], $exception[1]); } } /** * @dataProvider getValidMaterialsProviders */ public function testPutObjectTakesValidMaterialsProviders( $provider, $exception ) { $this->skipTestForPolyfillPhpVersions(); if ($exception) { $this->setupProvidedExpectedException($exception); } $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $kms = $this->getKmsClient(); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]), ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], ] ); $result = $uploader->upload(); $this->assertTrue($this->mockQueueEmpty()); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(self::TEST_URL, $result['ObjectURL']); } /** * @dataProvider getInvalidMaterialsProviders */ public function testPutObjectRejectsInvalidMaterialsProviders( $provider, $exception ) { $this->skipTestForPolyfillPhpVersions(); if ($exception) { $this->setupProvidedExpectedException($exception); } $s3 = $this->getS3Client(); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], ] ); $uploader->upload(); } /** * @dataProvider getValidMetadataStrategies */ public function testPutObjectTakesValidMetadataStrategy( $strategy, $exception, $s3MockCount ) { $this->skipTestForPolyfillPhpVersions(); if ($exception) { $this->setupProvidedExpectedException($exception); } $s3 = $this->getS3Client(); $i = 1; $results = []; while ($i++ < $s3MockCount) { $results [] = new Result(['ObjectURL' => 'file_url']); } $results []= new Result(['UploadId' => 'baz']); $results []= new Result(['ETag' => 'A']); $results []= new Result(['ETag' => 'B']); $results []= new Result(['ETag' => 'C']); $results []= new Result(['Location' => self::TEST_URL]); $this->addMockResults($s3, $results); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]), ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@MetadataStrategy' => $strategy, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], ] ); $result = $uploader->upload(); $this->assertTrue($this->mockQueueEmpty()); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(self::TEST_URL, $result['ObjectURL']); } /** * @dataProvider getInvalidMetadataStrategies */ public function testPutObjectRejectsInvalidMetadataStrategy( $strategy, $exception ) { $this->skipTestForPolyfillPhpVersions(); if ($exception) { $this->setupProvidedExpectedException($exception); } $s3 = $this->getS3Client(); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@MetadataStrategy' => $strategy, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], ] ); $uploader->upload(); } public function testPutObjectWithClientInstructionFileSuffix() { $this->skipTestForPolyfillPhpVersions(); $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['ObjectURL' => 'file_url']), new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]), ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@InstructionFileSuffix' => InstructionFileMetadataStrategy::DEFAULT_FILE_SUFFIX, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], ] ); $result = $uploader->upload(); $this->assertTrue($this->mockQueueEmpty()); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(self::TEST_URL, $result['ObjectURL']); } /** * @dataProvider getCiphers */ public function testPutObjectValidatesCipher( $cipher, $exception = null ) { $this->skipTestForPolyfillPhpVersions(); if ($exception) { $this->setupProvidedExpectedException($exception); } $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]), ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => $cipher, ], '@KmsEncryptionContext' => [], ] ); $result = $uploader->upload(); $this->assertTrue($this->mockQueueEmpty()); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(self::TEST_URL, $result['ObjectURL']); } /** * @dataProvider getKeySizes */ public function testPutObjectValidatesKeySize( $keySize, $exception ) { $this->skipTestForPolyfillPhpVersions(); if ($exception) { $this->setupProvidedExpectedException($exception); } $cipherOptions = [ 'Cipher' => 'gcm' ]; if ($keySize) { $cipherOptions['KeySize'] = $keySize; } $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); if (is_int($keySize)) { $bytes = $keySize / 8; } else { // Placeholder, client should throw for non-int key size $bytes = 1; } $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes($bytes), ]), ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => $cipherOptions, '@KmsEncryptionContext' => [], ] ); $result = $uploader->upload(); $this->assertTrue($this->mockQueueEmpty()); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(self::TEST_URL, $result['ObjectURL']); } public function testPutObjectAppliesParams() { $this->skipTestForPolyfillPhpVersions(); $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]) ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], 'before_initiate' => function($command) { $this->assertSame('foo', $command['Bucket']); $this->assertSame('bar', $command['Key']); $this->assertSame( 'kms+context', $command['Metadata']['x-amz-wrap-alg'] ); }, ] ); $result = $uploader->upload(); $this->assertTrue($this->mockQueueEmpty()); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(self::TEST_URL, $result['ObjectURL']); } public function testCanLoadStateFromService() { $this->skipTestForPolyfillPhpVersions(); $s3 = $this->getS3Client(); $url = 'http://foo.s3.amazonaws.com/bar'; $this->addMockResults($s3, [ new Result(['Parts' => [ ['PartNumber' => 1, 'ETag' => 'A', 'Size' => 4 * self::MB], ]]), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => $url]), ]); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]) ]); $state = S3EncryptionMultipartUploaderV2::getStateFromService( $s3, 'foo', 'bar', 'baz' ); $source = Psr7\Utils::streamFor(str_repeat('.', 9 * self::MB)); $uploader = new S3EncryptionMultipartUploaderV2( $s3, $source, [ '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], 'state' => $state ] ); $result = $uploader->upload(); $this->assertTrue($uploader->getState()->isCompleted()); $this->assertSame(4 * self::MB, $uploader->getState()->getPartSize()); $this->assertSame($url, $result['ObjectURL']); } public function testAddsCryptoUserAgent() { $this->skipTestForPolyfillPhpVersions(); $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $list = $s3->getHandlerList(); $list->appendSign(Middleware::tap(function($cmd, $req) { $this->assertStringContainsString( 'feat/s3-encrypt/' . S3EncryptionMultipartUploaderV2::CRYPTO_VERSION, $req->getHeaderLine('User-Agent') ); })); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]) ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [], ] ); $uploader->upload(); } public function testAddsUpdatedEncryptionContext() { $this->skipTestForPolyfillPhpVersions(); $s3 = $this->getS3Client(); $this->addMockResults($s3, [ new Result(['UploadId' => 'baz']), new Result(['ETag' => 'A']), new Result(['ETag' => 'B']), new Result(['ETag' => 'C']), new Result(['Location' => self::TEST_URL]), ]); $list = $s3->getHandlerList(); $list->appendSign(Middleware::tap(function( CommandInterface $cmd, RequestInterface $req ) { if ($cmd->getName() === 'CreateMultipartUpload') { $this->assertEquals( [ 'aws:x-amz-cek-alg' => 'AES/GCM/NoPadding', 'marco' => 'polo' ], json_decode( $req->getHeaderLine('x-amz-meta-x-amz-matdesc'), true ) ); } } )); $kms = $this->getKmsClient(); $keyId = '11111111-2222-3333-4444-555555555555'; $provider = new KmsMaterialsProviderV2($kms, $keyId); $this->addMockResults($kms, [ new Result([ 'CiphertextBlob' => 'encrypted', 'Plaintext' => random_bytes(32), ]) ]); $uploader = new S3EncryptionMultipartUploaderV2( $s3, Psr7\Utils::streamFor(str_repeat('.', 12 * self::MB)), [ 'bucket' => 'foo', 'key' => 'bar', '@MaterialsProvider' => $provider, '@CipherOptions' => [ 'Cipher' => 'gcm', ], '@KmsEncryptionContext' => [ 'marco' => 'polo' ], ] ); $uploader->upload(); } private function skipTestForPolyfillPhpVersions() { if (version_compare(PHP_VERSION, '7.1', '<')) { $this->markTestSkipped( 'The input sizes for the multipart uploader tests are too large' . ' to reasonably use with the AES-GCM polyfill that was added' . ' for PHP versions earlier than 7.1' ); } } }